Compare commits

...
Author SHA1 Message Date
Gergő MagyarandClaude Opus 4.8 2cf7e7fa88 chore: release v1.6.6 (#2075)
Bump gitnexus 1.6.5 -> 1.6.6 and add the 1.6.6 CHANGELOG section
covering ~190 PRs merged since v1.6.5 (range v1.6.5..main).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:44:28 +01:00
xianzuyang9-blipandGergő Magyar 1ca4f15267 fix: declare onnxruntime-common runtime dependency (#2074)
* Declare onnxruntime-common runtime dependency

* Declare onnxruntime-common runtime dependency

* Declare onnxruntime-common runtime dependency

* Declare onnxruntime-common runtime dependency

* Declare onnxruntime-common runtime dependency

* Remove package metadata unit test

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-08 08:44:05 +01:00
Gergő MagyarandClaude Opus 4.8 df5ce1f49b fix(ingestion): close remaining open language parsing-layer coverage gaps (#1919) (#2072)
* fix(c): skip computed #include MACRO instead of emitting a garbage import source (F5)

* fix(cpp): emit a Variable per name for structured-binding declarations (F9)

* fix(dart): extract static const/final class fields (F26)

* fix(dart): capture old-style function typedefs (F28)

* fix(dart): read real top-level variable shape instead of a dead type field (F29)

* fix(kotlin): capture callable references (F47)

* fix(kotlin): anchor infix-call capture to the operator only (F49)

* fix(kotlin): extract secondary constructors as members (F48)

* fix(kotlin): capture destructuring declarations (F51)

* fix(kotlin): index companion-object properties as fields (F52)

* test(kotlin): assert callable-reference coverage runs on the worker path (F47)

* fix(swift): extract protocol property requirements (F75)

* fix(swift): recognize enum_class_body as a method body node (F79)

* test(ingestion): rebaseline swift captures-golden + scope-capture fingerprints (#1919)

* fix(kotlin): attribute secondary-constructor body calls to the Constructor node (#1919 review CF1)

A Kotlin secondary constructor's body executes statements like a method body,
but the registry-primary scope-resolution path had no Function scope or
Constructor def for it. A call inside the body resolved its caller anchor up to
the enclosing Class scope, mis-attributing the CALLS edge to the class rather
than the Constructor.

Add `(secondary_constructor) @scope.function` to the Kotlin scope query so the
body becomes its own scope, and synthesize a `@declaration.constructor` (named
`constructor`, qualified `<Class>.constructor`, with parameter metadata) so the
scope owns a Constructor def that bridges to the structure-phase Constructor node.

Also add an arity-disambiguating lookup key for overloadable callables: two
same-name secondary constructors of different arity (e.g. a zero-arg vs a 2-arg)
share the qualified key whose first-write-wins assignment is source-order-
dependent — so a zero-arg overload could resolve to a sibling. The structure
node id encodes `#<arity>`; mirror that in the bridge keyspace and match by the
def's parameterCount. Same-arity overloads collapse onto one arity key exactly
as before, so no regression there.

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

* fix(kotlin): do not own function-local property bindings under the enclosing class (#1919 review CF3)

Kotlin emits destructuring / loop bindings (`val (a,b) = pair`,
`for ((k,v) in m)`) as `@definition.property` to dodge the block-scope
local-symbol pruner. When such a binding sits inside a method body of a class,
the structure-phase owner walk found the enclosing class and emitted a spurious
HAS_PROPERTY edge (e.g. `C -> k`), treating a function-local as a class member.

Guard the Property owner resolution: if a function-like ancestor is reached
before any class container, the property is function-local and gets no owner
edge (it falls back to a File DEFINES edge). Language-agnostic — genuine class
fields sit directly in the class body with no intervening function, so they
keep their HAS_PROPERTY owner edge.

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

* test(kotlin): guard non-companion property isStatic=false (#1919 review CF4)

Add a field-extraction case for a plain non-companion class
`class C { val x: Int = 1 }` asserting the property `x` has isStatic=false,
guarding the `isInsideKotlinCompanion` walk against false-positives.

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

* refactor(kotlin): dedup type_identifier lookup in extractOwnerName (#1919 review CF5)

The `node.namedChildren.find(c => c.type === 'type_identifier')?.text` lookup was
duplicated across the companion and non-companion branches of the Kotlin
field-extractor's extractOwnerName. Hoist it into a single local, preserving the
existing behavior (anonymous companion falls back to "Companion"; other nodes
prefer the `name` field, else the type_identifier text, else undefined).

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

* fix(dart): capture generic old-style function typedefs (#1919 review CF2)

* test(dart): guard multi-name field count and top-level-var labels (#1919 review CF4)

* docs(swift): correct isStatic comment re multi-modifier hasKeyword (#1919 review CF5)

* test(ingestion): rebaseline dart+kotlin scope-capture fingerprints after review remediation (#1919)

* fix(ingestion): correct CF3 owner-strip boundary set for accessor/init bodies and Dart signatures (#1919 review)

The CF3 property-ownership guard used FUNCTION_NODE_TYPES, which (a) includes
Dart bare signatures (function_signature/method_signature) — over-stripping
every Dart class getter/setter's HAS_PROPERTY owner — and (b) omits Kotlin
anonymous_initializer/getter/setter and Swift computed accessors — under-
stripping destructuring/locals inside init{} and accessor bodies, emitting
spurious Class->local HAS_PROPERTY edges. Introduces a guard-specific
LOCAL_SCOPE_BODY_NODE_TYPES set (signatures excluded, accessor/init bodies
included). Adds Dart accessor-ownership + Kotlin init/accessor destructuring
regression fixtures. Both confirmed on the worker pipeline; no cross-language
regression (1597 cross-language tests green).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:09:43 +01:00
Gergő Magyar 3963c497dd fix(parse): correct worker-pool docs drift + surface worker-side stack on crash (#2068) (#2070) 2026-06-08 07:20:12 +01:00
Gergő MagyarandClaude Opus 4.8 4fc2ffa5d0 refactor(ingestion): delete shadow-mode parity harness (RING4-3, #944) (#2071)
Ring 4 retires the legacy call-resolution DAG. With the legacy resolver
gone (RING4-1 #942, RING4-2 #943), shadow mode has nothing to dual-run
against, so the remaining shadow-mode artifacts are dead code.

- Delete gitnexus-shared/src/scope-resolution/shadow/{diff,aggregate}.ts
  (pure parity comparison logic) and its gitnexus-shared barrel exports.
- Delete the static parity dashboard (gitnexus/shadow-parity-dashboard/),
  which also removes the last GITNEXUS_SHADOW_MODE reference in the repo.
- Delete the shadow-mode unit tests (gitnexus/test/unit/shadow/).
- Scrub stale doc comments referencing the shadow harness / parity
  dashboard / removed legacy run (csharp/php/python/typescript index.ts,
  evidence.ts, module-scope-index.ts).

Already removed by RING4-1/-2 (verified): the shadow harness source and
GITNEXUS_SHADOW_MODE env handling; no CI job published dashboard artifacts.

Historical parity records preserved per acceptance: the CHANGELOG entry
(#918, #923, #951, #972) and the ci.yml RING4-1 note remain. Last
documented parity state is that historical coverage — no live
.gitnexus/shadow-parity/ run data exists in-tree (runtime output only).

Closes #944.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:06:42 +01:00
Abhigyan Patwari f0c292f9e7 perf(ingestion): prune inert local value symbols (#2065) 2026-06-07 14:47:51 +01:00
Gergő Magyar 2dc0cc6398 fix(mcp): prevent sibling-clone repo ID collisions and correct generated MCP tool names (#2067) 2026-06-07 10:57:15 +01:00
dependabot[bot]andGergő Magyar 2b4d6e742b chore(deps)(deps): bump @ladybugdb/core in /gitnexus (#2056)
Bumps [@ladybugdb/core](https://github.com/LadybugDB/ladybug) from 0.16.1 to 0.17.0.
- [Release notes](https://github.com/LadybugDB/ladybug/releases)
- [Commits](https://github.com/LadybugDB/ladybug/compare/v0.16.1...v0.17.0)

---
updated-dependencies:
- dependency-name: "@ladybugdb/core"
  dependency-version: 0.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-07 06:31:45 +01:00
SparshandGergő Magyar baca749e0b fix(vue): F89 JSDoc fix, F90 dual-script merge, F92 lang plumbing (#1936) (#2050)
* fix(vue): F89 JSDoc fix, F90 dual-script merge, F92 lang plumbing (#1936)

* fix(vue): reviewer fixes — P1 lang routing, P2 lineOffset, P2/P3 pipeline tests

* fix(vue): add jsx to lang routing condition

* fix(vue): update F90/F92 fixtures and test assertions for CI

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-07 06:02:30 +01:00
azizur100389 9a40af3d79 fix(java): dedupe inherited RequestMapping prefixes (#2057) 2026-06-07 05:28:10 +01:00
95f87fc12a perf(ingestion): Linux-kernel-scale analysis — worker-pool parse + finalize O(n²) + scope-resolution memory wall (#1983) (#2038)
* fix(ingestion): reduce parse-phase memory for huge repos (#1983)

Stop retaining full parse-cache chunks in RAM alongside the merged graph,
slim on-disk shards, defer worker ParsedFile emission for scope-resolver
languages, and add GITNEXUS_DEBUG_HEAP probes for OOM diagnosis.

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

* fix(ingestion): address #2038 tri-review findings (parse-phase memory)

Resolves the confirmed review findings on PR #2038:

- P1: thread exportedTypeMap through the sequential parse path
  (processParsingSequential) so a no-worker run over a partially-warm
  cache no longer silently drops the sequential-miss files' exported
  types. Cache hits made exportedTypeMap.size > 0, suppressing the
  end-of-loop buildExportedTypeMapFromGraph rebuild, but the sequential
  path never populated the map. Regression test added (fails on the
  pre-fix tree, passes after) plus a fully-sequential differential oracle.
- P2: saveParseCache builds its on-disk index from hashes actually
  written/copied (writtenKeys), never a usedKeys hash whose shard write
  or copy was skipped — no more phantom index entries.
- P2: add a unit test asserting SCOPE_RESOLUTION_LANGUAGES stays in sync
  with SCOPE_RESOLVERS (asymmetric drift would lose a language's ParsedFile).
- Backfill cache coverage: loadParseCacheChunk missing/corrupt -> undefined,
  pruneCache onDiskKeys branch, slim preserves nodes, saveParseCache
  copy-evicted-shard round-trip.
- Cleanups: single-source heap-probe gating via isDebugHeapEnabled();
  hoist the per-chunk mkdir in persistParseCacheChunk behind a
  process-scoped Set; gate COBOL's unused worker-side ParsedFile
  extraction (graph nodes still come from cobolPhase) while keeping
  fileCount/progress unconditional.

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

* refactor(ingestion): remove dead worker-side ParsedFile extraction

After #2038 gated worker `ParsedFile` emission behind `!isScopeResolutionLanguage(language)`, and with all 16 SupportedLanguages registered in SCOPE_RESOLVERS, that gate was structurally always true — the worker already produced no ParsedFiles and scope-resolution re-extracts each file from source on the main thread (run.ts). Remove the now-dead machinery:

- Drop both worker `extractParsedFile` call-sites (tree-sitter processFileGroup + the standalone-provider branch) and the `result.parsedFiles.push`. The standalone branch keeps fileCount/onFileProcessed per file. `result.parsedFiles` stays declared but empty (field removal deferred).
- Remove the now-orphaned `scopeSourceKind` var + `ScopeCaptureSourceKind`/`extractParsedFile`/`isScopeResolutionLanguage` imports.
- Delete the consumerless `migrated-languages.ts` (isScopeResolutionLanguage + SCOPE_RESOLUTION_LANGUAGES) and its drift-guard test — parse-worker was their only importer. Also improves AGENTS.md "shared ingestion code must not name languages" compliance.

`extractParsedFile` and the scope-extractor-bridge stay (scope-resolution/run.ts + Vue resolver use them). Behavior-preserving: worker-sequential-parity passes before and after; tsc/eslint clean; no baseline/golden drift.

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

* refactor(ingestion): worker-pool-only parsing; remove sequential parser (#1983)

Completes the #1983 huge-repo parse-OOM effort by making the worker pool
GitNexus's sole parse path.

Parallel serialization (the perf core): workers serialize their ParsedFiles to
a disk store in parallel and stream them back to scope-resolution, so the main
thread no longer re-parses every file (the tree-sitter native-memory leak that
caused the OOM). Adds chunk merge-pipelining + work-proportional chunk sizing so
the pool stays saturated.

Remove the sequential parser: `--workers 0`, `GITNEXUS_WORKER_POOL_SIZE=0`, and
`skipWorkers` now hard-error (no silent degrade — #1741); the small-repo
threshold no longer selects an in-process path; pool creation stays lazy /
cache-miss-gated so warm all-hit runs never spawn workers.

Worker-path parity fixes — removing sequential surfaced two pre-existing gaps
that tiny-fixture tests had masked by running below the worker threshold, both
fixed by carrying per-file metadata as DATA across the worker boundary (never
re-parsing on the main thread, preserving the OOM fix):
  - C++: templateConstraints wired into worker node identity (SFINAE overload
    disambiguation) + ADL / inline-namespace capture side-channel serialized
    onto the ParsedFile.
  - Kotlin: companion-scope side-channel serialized the same way (companion /
    static dispatch).

Validation: tsc + build clean; full suite green (10,190 pass — the only
deterministic failures were the now-fixed C++/Kotlin worker-path gaps; the 2
remaining full-run failures are pre-existing load flakiness, green in
isolation); cpp-pipeline benchmark stays linear on a 1-worker pool.

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

* fix(ingestion): wire C static-linkage side-channel + ADL O(1) collect + tri-review cleanups (#1983)

Follow-up to the worker-pool-only refactor, from a tri-review of the parse path.

- C static-linkage side-channel (P1): cProvider had no collect/applyCaptureSideChannel,
  so on the now-sole worker path C `static` file-local marks were lost across the worker
  boundary -> false cross-file CALLS edges + over-broad #include wildcard visibility on
  every C analysis (the Linux kernel is C). Mirror the C++/Kotlin wiring: serialize
  `staticNames` per file onto ParsedFile.captureSideChannel and restore it on the main
  thread (no re-parse). + a worker-path regression test (the existing c-static-isolation
  fixture passed vacuously — its collision resolves via #include before the global
  free-call fallback ever consults static-linkage).

- captureSideChannel `kind` discriminant: add `kind:'cpp'`/`kind:'c'` tags + guards
  (Kotlin already had one) now that C/C++/Kotlin share the single generic field.

- Perf: collectCppAdlSideChannel scanned the whole argInfoBySite/noAdlSites maps per file
  (O(F^2) per sub-batch, ~100M parseSiteKey calls at kernel scale). Add per-filePath
  lockstep indexes -> O(1) collect; serialized snapshot byte-identical.

- Cleanups: inline the one-line processParsingWithWorkers wrapper into processParsing;
  drop the always-empty WorkerExtractedData.calls/assignments/constructorBindings fields;
  remove the voided astCache param from processParsing; refresh stale "sequential
  fallback" JSDoc.

Validation: tsc + build clean; cpp 297/297, c 8/8 (incl. the new worker-path
static-linkage guard), typescript + parsedfile-store green; cpp ADL benchmark stays linear.

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

* perf(scope-resolution): index C/C++ #include resolution in finalize (O(n²)→O(n))

Kernel-scale C/C++ analysis ground in finalizeScopeModel because three
per-#include operations each did a full O(F) scan with no index — the
finalize O(n²) that surfaced once the #1983 parse-phase OOM was fixed:

- expand{C,Cpp}WildcardNames: parsedFiles.find() per wildcard edge → O(R·F)
- resolveImportTarget: new Set(allFilePaths) rebuilt per #include
- resolveCImportTarget: suffix-match scanned all workspace paths

Each is replaced with a WeakMap-per-pass index keyed on the stable
parsedFiles/allFilePaths references that scope-resolution run.ts passes
once per pass:

- Map<ScopeId,ParsedFile> for wildcard expansion (c/static-linkage.ts +
  cpp/file-local-linkage.ts)
- memoized augmented header set (c/scope-resolver.ts + cpp/scope-resolver.ts)
- basename-bucketed suffix index in resolveCImportTarget (c/import-target.ts),
  shared by C and C++ since resolveCppImportTarget delegates to it

Collapses the C/C++ finalize from O(R·F) to O(R+F). Pure-perf, byte-identical
edge output: 962 targeted tests green (490 C + 472 C/C++ scope-resolution);
the basename index preserves the exact endsWith('/'+target) match and the
fewest-path-components-then-lexicographic tie-break.

The kernel's ~25-30k .h headers are classified C++, so both providers must
be fixed. Proven on the Linux kernel: the C finalize completed
(sr-post-finalize lang=c → sr-end lang=c), which the pre-fix run never
reached in 16+ min of grinding.

Build-independent follow-ups (separate from this finalize fix), documented
for later: emitFreeCallFallback same-name buckets (emit phase),
buildGraphNodeLookup + precount global setup, the ParsedFile store-load,
the dart/go/ruby expand-wildcards .find siblings, and the ~26GB
scope-resolution memory floor (full kernel completion needs >~40GB RAM).

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

* test(bench): regenerate C scope-capture baseline for the #1983 c-static-linkage-worker fixture

bench/scope-capture/measure.mjs fingerprints emitCScopeCaptures over the
lang-resolution/c-* fixture corpus. The #1983 PR added the
c-static-linkage-worker fixture (caller.c/lib.c/lib.h/local.c — the
worker-path static-linkage side-channel test) but did not regenerate the C
baseline, so `--check` has been red on this branch (main, lacking the
fixture, still matches 0de009b).

Pure fixture-corpus drift — no c/captures.ts or query change branch-vs-main,
existing fixtures' captures byte-identical (c-captures.test.ts 45/45),
scaling stays linear (~0.97). Regenerated: 0de009b -> 39f3a83. Bench now
PASS (14 languages). Unrelated to the finalize O(n²) fix.

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

* perf(scope-resolution): lower kernel-scale resident memory floor + setup cost

Reduce the scope-resolution resident-memory floor and setup throughput on
huge repos (Linux kernel), the wall that remains after #1983 (parse OOM) and
the finalize O(n^2) fix (b71c77b8). Five units; all preserve byte-identical
edge output (C fixture 177n/255e + c/cpp/cross-file/php/static-linkage suites
green, 619 tests).

U1 (src/cli/analyze.ts): RAM-aware auto heap-cap. Replace the hardcoded
16384MB cap with computeHeapCapMb = max(16384, floor(0.75*effectiveRAM)),
where effectiveRAM = min(os.totalmem(), process.constrainedMemory()) with the
unconstrained-sentinel guard. Add --max-semi-space-size=128 on the respawn.
A user-supplied NODE_OPTIONS heap still wins (no re-exec). Verified: 23973MB
on a 31964MB box, 16384 floor on small machines, cgroup-aware, sentinel safe.

U2 (src/storage/parsedfile-store.ts, .../pipeline/phase.ts): export forceGc()
and call it at the per-language eviction boundary, so a finished language's
ParsedFiles are reclaimed before the next language's store-load instead of
collected lazily under the next pass's allocation pressure (which at cap>=RAM
degrades into swap-thrash). Measured on a real drivers/net/ethernet run:
C 2113->894MB and C++ 1754->1057MB reclaimed at the boundary (no fragmentation
defeat). Answers the plan's Open Question 1.

U3 (src/storage/parsedfile-store.ts): intern def objects by nodeId in the load
reviver so a SymbolDefinition's three serialized copies (localDefs /
scope.ownedDefs / scope.bindings[].def) collapse to one shared object on load.
Per-shard def pool (a def's copies are shard-local). Measured ~42% off the
def-object retained heap (3->1; 1.8M->600k distinct objects on 600k defs).

U4 (.../passes/free-call-fallback.ts): memoize pickUniqueGlobalCallable's
post-filter candidate list per (name, callerFilePath), only when no per-caller
visibility filter applies (the list is then a pure function of name+file), so
repeated free calls of one name from a file reuse the same-name-bucket scan
instead of re-walking a potentially huge bucket per site. The cached array is
read-only-consumed by the .filter()-based arity/overload narrowers. Exported
pickUniqueGlobalCallable + buildGlobalCallableIndex and added an equivalence
test (memoized == un-memoized reference for every (name, file, arity),
including warm-cache repeats and cross-file file-local exclusion).

U5 (.../pipeline/phase.ts): replace the O(L*F) per-language precount + repeated
scannedFiles.filter() with a single O(F) partition-by-language pass; bracket
buildGraphNodeLookup with scope-setup-nodeLookup heap probes so the long setup
is no longer silent.

Plan: docs/plans/2026-06-06-001-perf-kernel-scope-resolution-memory-plan.md
(U6 out-of-core global index deferred). Note: the kernel's full C++ pass floor
(~20k headers + the 8.8GB graph) likely still exceeds 24GB by itself, which is
why U6 remains the only unit that clears the wall.

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

* fix(test): match OOM-guidance e2e assertions to the U1 reworded hint

The analyze-heap-oom-e2e real-child-OOM test still asserted the pre-U1
wording ('...out of memory.' + a hardcoded 24576 cap). U1 reworded the hint
to mention the auto heap-cap and use a <MB> placeholder, so the three
toContain substrings no longer matched (the assertion at line 62 failed on
all platforms). Update them to the current message. The unit twin
(analyze-heap-respawn) was already updated in 85bfc216; this integration
test was missed by the targeted local run.

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

* perf(lbug): U6a — deterministic id-sorted graph output behind GITNEXUS_SORT_GRAPH_OUTPUT

First increment of U6 (out-of-core scope-resolution). Adds an optional
deterministic ordering of node + relationship CSV rows by their unique graph
id, behind GITNEXUS_SORT_GRAPH_OUTPUT (default OFF = today's graph-insertion
order, byte-identical — the iterator is returned untouched). With the flag ON
the CSV becomes a pure function of the node/edge SET rather than of emit order.

This is the structural enabler for the windowed/out-of-core resolve (U6b-U6d):
csv-generator.ts:518 currently iterates graph.iterRelationships() in insertion
order with NO terminal sort, so any deviation from parsedFiles-order emit would
change bytes. With U6a on, a windowed emit need only reproduce the same edge
SET, not the global insertion order — removing the single largest byte-identical
hazard from every later windowing step.

Verified: default off keeps the existing csv-pipeline suite byte-identical; on,
node rows are id-sorted and output is independent of graph insertion order
(set-build) with the same node/edge set.

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

* perf(storage): U6d foundation — disk-backed scope store + lazy ScopeTree

Adds scope-index-store.ts: persistScopeShards (per-file scope shards via the
proven mapReplacer + def-interning reviver) + DiskBackedScopeTree, a lazy
ScopeTree that serves getScope from a bounded LRU of decoded shards plus a small
resident skeleton (scopeId -> {shard, childIds, parent}). Exports
makeInterningReviver from parsedfile-store for reuse.

This is the contained, highest-risk mechanism of U6d (out-of-core scope
resolution): the emit passes reach the heavy per-Scope binding payload
(~17-20GB on the kernel) ONLY through scopeTree.getScope (a point lookup) and
getChildren — they never read parsed.scopes directly — so moving that payload to
disk behind getScope is transparent. Every consumer reads a Scope BY VALUE, so a
value-faithful disk round-trip is byte-identical to resolution.

Proven in isolation: DiskBackedScopeTree is value-identical to buildScopeTree
for getScope/getChildren/getParent/getAncestors/has/size across multiple files
and after LRU eviction, and preserves the def-identity collapse (ownedDefs[i]
=== binding.def). Nothing wires it yet (the resolution-pipeline integration is
the next increment) — zero production impact; default off.

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

* perf(scope-resolution): U6d integration — seal scopeTree to disk before emit (GITNEXUS_DISK_SCOPE_INDEX)

Wires the U6d out-of-core scope index into the live pipeline behind
GITNEXUS_DISK_SCOPE_INDEX (default OFF = byte-identical). When on:

- finalize-orchestrator builds a TransitionalScopeTree (validated, fully
  resident) instead of buildScopeTree, so finalize/propagate/resolve are
  unchanged.
- After resolve, before emit, run.ts seals it: persists the scopes to a
  file-sharded scope-index-store, swaps the model's scopeTree to disk-backed
  serving from the inside (the frozen bundle can't be reassigned, but the
  wrapper nulls its own resident backing), and drops the heavy Scope.bindings
  payload from all THREE holders — the model's tree (seal), the caller's
  preExtractedParsedFiles, and run.ts's own parsedFiles (scope-stripped copies
  for emit). Emit reads scopes only via scopeTree.getScope (a point lookup,
  now disk-backed + LRU) — verified it never reads parsed.scopes.

Purpose: lower the per-language resident PEAK (kernel C pass ~20→~12 GB by
moving the ~8-9 GB scope payload to disk) so the analysis fits on smaller-RAM
machines. At >=24 GB the full kernel already fits with U1-U5 (U2's 8.7 GB
inter-language forceGc reclaim keeps each pass under cap) — empirically
confirmed — so this is the sub-24 GB lever, not needed at 24 GB.

Byte-identical evidence: DiskBackedScopeTree/TransitionalScopeTree return
value-identical scopes vs buildScopeTree (getScope/getChildren/getParent/
getAncestors, across files + after LRU eviction + post-seal); emit reads only
getScope + referenceSites; flag-off (394 tests) and flag-on-resident (91 tests)
resolver suites stay green; an end-to-end A/B on a 212-file C+cpp+rust subset
produced identical 17,444 nodes / 31,343 edges with the seal firing per language
(c: 410→141 MB reclaimed). Kernel-scale peak-drop measurement pending the
in-flight verdict run freeing memory.

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

* perf(scope-resolution): U6d — id-back workspaceIndex so the disk seal can reclaim scopes

The kernel run revealed the contained scopeTree seal didn't lower the heap:
WorkspaceResolutionIndex held Scope OBJECTS (classScopeByDefId / moduleScopeByFile),
built from every ParsedFile and live through emit, so the ~28k module + class
scopes stayed pinned past the seal (sr-seal-pre 17,583 -> sr-seal-post 17,771 MB,
no drop). It was the sole residual Scope-object holder (SemanticModel holds none).

Fix: classScopeByDefId / moduleScopeByFile become id-backed ScopeByKeyView
instances — a ReadonlyMap<K, Scope> facade over a K->ScopeId map + the scopeTree,
whose .get fetches via scopeTree.getScope(id). The index now pins only ids, so
once the tree seals to disk the scopes become collectible. Byte-identical: the
view returns the same Scope the resident tree holds (or a value-identical revived
one in disk mode), and iteration keeps the old insertion order. buildWorkspace
ResolutionIndex takes an optional scopeTree (live pipeline passes it); without it
(unit tests) the legacy direct Scope-object maps are returned unchanged.

Verified byte-identical: 733 tests across workspace-index / imported-return-types
/ c / cpp / cross-file / go / java. Kernel peak-drop re-measurement to follow.

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

* perf(scope-resolution): U6d — precompute exportedCallableByName (fix disk-getScope thrash)

The workspaceIndex id-backing freed the kernel scopes but exposed a throughput
collapse: findExportedDefByName's workspace fallback (walkers.ts:1019) scanned
EVERY module scope's bindings per unresolved free call, and under the U6d
disk-backed scopeTree each module-scope access faulted a shard in from disk —
lib ON went ~1min -> ~7.5min.

Fix: precompute the fallback result once into
WorkspaceResolutionIndex.exportedCallableByName (simpleName -> first module-local
callable def, first-file-wins — the exact semantics the scan returned), built
from the resident module-scope bindings at index-build time. findExportedDefByName
now does an O(1) lookup with zero disk reads.

Result: lib ON ~7.5min -> 21s (cache-warm), byte-identical 17,444/31,343; 758
tests green across workspace-index + c/cpp/cross-file/go/python.

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

* docs: rename cryptic U-unit codes to descriptive names in comments

The plan-unit shorthand (U3/U4/U6a/U6d/...) was meaningless in the code.
Renamed in comments + test descriptions (no behavior change, byte-identical):
  out-of-core scope index   (was U6)
  deterministic output      (was U6a)
  disk-backed scope seal    (was U6d)
  def-object interning      (was U3)
  free-call candidate cache (was U4)
Also renamed throughout the PR title/summary. Pushed commit messages keep
their original U-codes as historical record.

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

* fix(ingestion): durable ParsedFile shards for warm-cache coverage (#2038)

On a warm re-analyze where every chunk is a parse-cache HIT, no parse worker
runs, the run-scoped ParsedFile store is cleared at parse start, and the cached
ParseWorkerResult carries no ParsedFiles (the worker writes them to the store
and empties them from the message). Scope-resolution then found an empty store
and fell back to main-thread extractParsedFile — re-opening the #1983
tree-sitter native-leak OOM the disk store closes (abhigyanpatwari review on
parse-cache.ts).

Fix: workers ALSO write their ParsedFiles to a durable, content-addressed store
(parsedfile-cache/) keyed by chunk hash, mirroring the parse cache's lifecycle
(version-gated by PARSE_CACHE_VERSION, pruned in lockstep to the surviving
keys). On a warm hit the chunk's durable shards are byte-COPIED into the
run-scoped store (no re-parse, no re-serialize -> byte-identical), so
scope-resolution streams them exactly as on a cold run. A coherence gate
re-dispatches the worker whenever a cached chunk's durable shards are missing
(migration / pruned / version-stale) -- never the main-thread extract.

- worker-pool/parse-worker: thread chunkHash through dispatch->job->flush
  (incl. split/requeue) so the worker tags its durable shard by content
- parsedfile-store: durable persist / restore / index / prune API (sibling
  dir, never cleared per run); content-addressing makes stale reuse impossible
- parse-impl: load durable index, gate the cache hit on durable coverage,
  restore on hit, dispatch chunkHash on miss
- run-analyze: prune+save the durable store to the parse cache's surviving keys
- saveParseCache returns its written keys (the durable keepKeys)

Verified on linux/lib: warm preExtractedHits = full coverage (520/207/1, zero
main-thread re-parse), byte-identical cold==warm (17,456n/31,353e), warm 8.5x
faster. New two-run + mixed-mode + coherence-gate regression test.

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

* fix(ingestion): clear stale scope-index-store shards on each seal (#2038)

The disk-backed scope index writes sequential s<n>.json shards into a shared
<storagePath>/scope-index-store/ dir, with the index resetting per
persistScopeShards call. A seal that writes fewer shards than a previous one
(a later language with fewer files, or a re-run of a shrunken repo) left stale
tail shards on disk indefinitely -- never read by the disk-backed tree, but
multi-GB on kernel-scale repos.

Add clearScopeIndexStore() and clear at the start of persistScopeShards: the
previously sealed language has finished emit and been released before the next
seal runs, so its DiskBackedScopeTree never reads those shards again. Unit
tests: a stale prior-run shard is removed, a fewer-files re-seal leaves no tail
shards, and the helper is idempotent.

Addresses abhigyanpatwari review on run.ts (disk hygiene for the
GITNEXUS_DISK_SCOPE_INDEX path).

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:46:34 +01:00
evolution 3b43eb8b47 fix(go): capture multi-name declarations (#2032) 2026-06-06 04:47:17 +01:00
bb3642ad5f fix(rust): F70 — replace struct_expression name:(_) with three specific patterns (#2051)
* fix(rust): F70 — replace struct_expression name:(_) with 3 specific patterns

* fix(rust): F70 — cover scoped+turbofish struct literals (foo::Bar::<T> {})

The three patterns enumerate struct_expression.name as type_identifier /
scoped_type_identifier / generic_type_with_turbofish, but
generic_type_with_turbofish.type can itself be a scoped_identifier
(e.g. foo::Bar::<i32> {}), which the turbofish pattern — requiring
type:(type_identifier) — did not match. That dropped the constructor
reference entirely (verified: emitRustScopeCaptures returns 0 ctors for
foo::Bar::<i32> {} and a::b::Bar::<i32> {}).

Add a fourth pattern that captures the trailing identifier of the scoped
turbofish path (scoped_identifier.name is an identifier, not a
type_identifier), and correct the comment that claimed all cases were
covered.

Strengthen rust-f70.test.ts: assert exactly one constructor per case, add
negative assertions guarding against the old full-path capture, and add
the scoped+turbofish and crate:: cases.

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

---------

Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:46:07 +01:00
Anton FedotovandGergő Magyar 782f70cc07 feat(wiki): add opencode local provider (#2039)
* feat(wiki): add opencode local provider

* style(wiki): format local cli client

* fix(wiki): harden opencode event parsing

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-05 09:24:00 +01:00
jwcrystal 22304cd4a4 fix(mcp): prevent orphan processes by handling stdin close/end and startup race condition (#2049)
* fix(mcp): prevent orphan processes by handling stdin close/end and startup race condition

Three gaps in stdin EOF handling:

1. Startup race: parent can die before `process.stdin.on("end", ...)` is
   registered, so the event is missed entirely.
2. Missing "close" event: when pipe is forcibly closed (parent SIGKILL),
   "close" fires without "end" on some platforms.
3. Transport layer did not propagate stdin termination to its onclose
   callback.

Fixes:
- Check readableEnded/destroyed in start() before registering listeners.
- Register stdin end+close listeners in CompatibleStdioServerTransport.
- Add _closed guard for idempotent close().
- Throw if start() is called after close().
- Add process.stdin.on("close") in server.ts alongside existing handlers.
- Add 5 regression tests.

* fix(mcp): register stdin shutdown before server connect
2026-06-05 08:36:28 +01:00
89b02286ad fix(csharp): qualified/alias constructor names, : base/: this initializers, generic type-arg strip (#2046)
* fix(csharp): bind qualified constructor names, capture : base/: this, fix generic strip

Mirrors the Java #1928 parsing-layer fixes for the C# scope-resolution path —
the same three defect classes exist verbatim in C#:

- Qualified / qualified-generic / alias-qualified constructor calls
  (`new Ns.Foo()`, `new A.B.Foo()`, `new Ns.Box<int>()`, `new MyAlias::Foo()`,
  `new global::Foo()`) bound only `@reference.call.constructor.qualified` with no
  `@reference.name`, so the central extractor fell back to the whole-expression
  anchor and the reference name became the raw `new Ns.Foo()` text (never
  resolved). Derive the simple-name tail via the existing `terminalTypeNameNode`
  helper (handles qualified_name, generic tail, and alias_qualified_name), and
  add a query arm for the top-level `alias_qualified_name` shape that was not
  captured at all.

- `: base(...)` / `: this(...)` explicit constructor initializers, modeled by
  tree-sitter as `constructor_initializer` and never matched by the scope query,
  dropped the chained-constructor CALLS edges. Synthesize them: `this` → enclosing
  type name; `base` → the base type's bare name (first base-list entry, which C#
  requires to be the base class). Arity attached for overload disambiguation.

- `interpretCsharpTypeBinding`'s qualifier strip used `lastIndexOf('.')` over the
  whole string, cutting inside a qualified generic type ARGUMENT
  (`Dictionary<string, Ns.User>` → `User>`). Make stripQualifier generic-aware:
  reduce only the segment before the first `<`, re-attaching the generic suffix —
  multi-arg generics stay intact so the `.Values`/`.Keys` collection-accessor
  unwrap keeps working.

Tests: capture-level unit tests for every constructor shape (incl. alias-qualified,
double-match guard) and `: base`/`: this` (incl. struct/record/mixed-base);
interpretCsharpTypeBinding unit tests (the corruption case + nullable/nested/
unknown-generic edges); end-to-end resolver tests with new fixtures. The
csharp-captures golden was regenerated — drift is purely additive (only the new
fixtures; zero existing-fixture digests changed).

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

* fix(csharp): enhance constructor resolution and namespace qualification

- Implemented qualified constructor name binding to resolve collisions between types in different namespaces.
- Added support for `: base(...)` and `: this(...)` constructor initializers to ensure correct edge emission in the scope resolution.
- Improved generic argument stripping to prevent incorrect parsing of qualified types.
- Introduced tests for new features, including handling of interface-only base classes and qualified constructor calls.

This update addresses issues related to constructor resolution and namespace qualification, ensuring accurate type references in C# code. Tests have been added to validate these changes.

* fix(csharp): implement namespace prefix tagging for file-level type definitions

- Updated the C# ingestion process to tag file-level type definitions with their enclosing namespace path using a new `namespacePrefix` field, without altering the `qualifiedName`.
- Enhanced the scope resolver to utilize the `namespacePrefix` for resolving same-tail collisions in constructor calls, improving accuracy in type resolution.
- Added unit tests to validate the new functionality, ensuring that namespace prefixes are correctly applied to both block-scoped and file-scoped types, while leaving namespace-free types untagged.

This change addresses issues related to namespace qualification and constructor resolution in C# code, facilitating better handling of type references.

* refactor(scope-resolution): share isOverloadableCallable via util

Extract the ctor/function/method overload predicate into
callable-labels.ts so graph-bridge registration and lookup stay aligned
without duplicated private copies in ids.ts and node-lookup.ts.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-05 07:04:57 +01:00
281ce2600c fix(java): close parsing-layer coverage gaps F35/F38/F41 (#1928) (#2045)
* fix(java): close parsing-layer coverage gaps F35/F38/F41 (#1928)

Registry-primary scope-resolution path (the live one post-#942/#943):

- F35 [HIGH]: qualified / qualified-generic constructor calls. `new pkg.Foo()`
  parses as a `scoped_type_identifier` that the query bound only as
  `@reference.call.constructor.qualified` with no `@reference.name`, so the
  scope extractor fell back to the whole-expression anchor and the reference
  name became the raw `new pkg.Foo()` text (never resolved). Bind the simple
  -name tail (end-anchored last child) and add an arm for the previously
  uncaptured `new pkg.Box<String>()` (qualified + generic) shape.

- F38 [MEDIUM]: `super(...)` / `this(...)` explicit constructor invocations,
  modeled as `explicit_constructor_invocation` and never matched by the scope
  query, dropped the chained-constructor CALLS edges. Synthesize them with the
  target resolved structurally (this -> enclosing type name; super -> superclass
  tail via the shared javaBaseLookupNameNode, skipping implicit Object) plus
  arity for overload disambiguation.

- F41 [LOW]: interpretJavaTypeBinding stripped the qualifier before generics, so
  a qualified generic type arg (`Map<String, com.example.User>`) was cut inside
  the generic into `User>`. Strip generics first, then the qualifier; make the
  erasure fallback qualifier-tolerant.

F36/F37 already landed upstream (#1940/#1956); F39/F40 are legacy-bank remnants
that are no longer consumed (legacy @import skipped in parse-worker; legacy
@call never read in parse-impl) so they are intentionally left untouched.

Tests: low-level capture unit tests (constructor shapes incl. double-match
guard; super/this/enum/implicit-Object), interpretJavaTypeBinding unit tests
(qualified generic args + the corruption case), and end-to-end resolver tests
with new fixtures asserting the CALLS edges resolve to the correct constructors.

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

* fix(scope-resolution): register Constructor overload keys so this()/super() chains don't self-loop (#1928 F38 review)

Review of #2045 caught two gaps; both confirmed by reproduction.

P2 — F38 this() emitted a self-loop. On the java-explicit-constructor fixture,
Child(int){ this(); } produced CALLS Child()#0 -> Child()#0 instead of
Child(int)#1 -> Child()#0. Root cause is the language-agnostic graph-bridge: the
parse phase mints distinct Constructor nodes (Child#0, Child#1) carrying
parameterTypes, but node-lookup.ts registered the parameter-types / shape
overload keys only for Function/Method, never Constructor, so both ctors
collapsed onto the first-wins qualified/simple key and the caller Child(int)
resolved to Child#0 (the this() target). Extend the overload keys to Constructor
in both node-lookup.ts (registration) and ids.ts (lookup) via a shared
isOverloadableCallable predicate. Verified the edge now connects distinct nodes
(Child#1 -> Child#0); super(1)->Base#1 still correct. No cross-language
regressions (the 9 worker-path failures reproduce identically on clean HEAD).

Also harden the integration test: it matched the this() edge on name only, which
a self-loop satisfies; now assert the endpoints are DISTINCT constructors.

P3 — F41 order-regression guard was inert (List<Map<String,User>> normalizes to
List under both strip orders). Add List<com.x.Foo<String>> -> List, which is
corrupted to Foo<String>> under the old order and only correct generics-first.

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

* fix(java): update fingerprint and add notes for constructor query captures in baselines.json

Updated the fingerprint for the Java section and added detailed notes regarding the enhancements in constructor query captures, including qualified and qualified-generic constructor queries. This change reflects ongoing improvements in the parsing layer coverage and fixture updates.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-05 06:39:12 +01:00
azizur100389 93e04b46d6 fix(lbug): load FTS in Windows read pool (#2040) 2026-06-05 04:23:21 +01:00
dependabot[bot] 54ea21a0bc chore(deps)(deps): bump hono from 4.12.18 to 4.12.23 in /gitnexus (#2044)
Bumps [hono](https://github.com/honojs/hono) from 4.12.18 to 4.12.23.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.18...v4.12.23)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 22:37:34 +01:00
dependabot[bot] 1812fcd165 chore(deps)(deps-dev): bump vitest from 4.1.7 to 4.1.8 in /gitnexus (#2043)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.7 to 4.1.8.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 22:37:24 +01:00
dependabot[bot] 4d010ff3f0 chore(deps)(deps-dev): bump @vitest/coverage-v8 in /gitnexus (#2042)
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.7 to 4.1.8.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 22:37:14 +01:00
azizur100389 3b195ec100 fix(csharp): normalize primary base receiver type (#2036) 2026-06-04 18:57:34 +01:00
Gergő MagyarandClaude Opus 4.8 bd59fa95ce refactor(ingestion): delete legacy resolution context + tiered-lookup plumbing (RING4-2, #943) (#2033)
* test(ingestion): characterize Laravel route → controller CALLS edges (RING4-2 #943)

Pins the current processRoutesFromExtracted edge-emission behavior (which had
no direct coverage) before migrating it off the legacy ResolutionContext.resolve
tiered lookup. Locks edge target, reason, and confidence values.

* refactor(ingestion): resolve Laravel route controllers via type registry (RING4-2 #943)

Migrate processRoutesFromExtracted off the legacy ResolutionContext.resolve
tiered lookup onto model.types.lookupClassByName (global class resolution) +
model.symbols.lookupExactAll (same-file method lookup). Drops the TIER_CONFIDENCE
dependency for a fixed ROUTE_EDGE_CONFIDENCE constant matching the prior
global-tier confidence. Characterization tests (6) stay green — behavior preserved.

* refactor(ingestion): delete ResolutionContext.resolve tiered lookup (RING4-2 #943)

Removes the legacy tiered name resolution — resolve/resolveUncached,
TieredCandidates, ResolutionTier, TIER_CONFIDENCE, walkBindingChain, the
package-dir index, the per-file resolve cache, and tier-hit stats. The context
is now a thin holder for the live SemanticModel plus the (now-dead) per-file
import maps, which the follow-up prune removes.

Deletes the dedicated resolution-context.test.ts and symbol-resolver.test.ts
(both exercised the removed .resolve tiered lookup). Full unit suite green
(the 3 analyze worker-pool tests are pre-existing load flakes — pass isolated).

* refactor(ingestion): delete legacy import-map plumbing + wildcard synthesis (RING4-2 #943)

The per-file importMap / namedImportMap / packageMap / moduleAliasMap that fed
the retired tiered resolver are now dead — nothing reads them (IMPORTS edges
come from scope-resolution's imports-to-edges bridge, independent of these
maps). Removes:
  - wildcard-synthesis.ts (synthesized the dead namedImportMap/moduleAliasMap)
  - import-processor's resolution path (processImports/processImportsFromExtracted/
    wireImplicitImports/buildImportResolutionContext), keeping only the live
    preprocessImportPath path-cleanup helper
  - the parse-impl orchestration that drove them

The parse phase now threads its SemanticModel to scope-resolution directly
(parseOutput.model) instead of wrapping it in the resolution context. Deletes
the obsolete wildcard/import-processor unit tests; trims the dead processImports
cases from sequential-language-availability (processParsing coverage kept).

* refactor(ingestion): delete resolution context + named-binding plumbing (RING4-2 #943)

Completes the legacy-resolution retirement. With the tiered resolver gone, the
entire per-file import-extraction chain is dead — its only consumer was the
deleted ResolutionContext.resolve, and scope-resolution emits IMPORTS edges
from its own finalized ImportEdges:

  - delete model/resolution-context.ts (the legacy context); the parse phase
    now hands its SemanticModel to scope-resolution as parseOutput.model
  - delete the named-bindings/ extractors + the namedBindingExtractor provider
    hook (built the dead NamedImportMap) across all 8 providers + the worker
  - delete the orphaned implicitImportWirer hook + Swift implementation +
    providersWithImplicitWiring (scope-resolution owns implicit imports now)
  - drop the dead ExtractedImport type + worker/sequential import accumulation
    (result.imports / WorkerExtractedData.imports)
  - import-processor.ts and its preprocessImportPath helper are now unreferenced

Deletes the obsolete named-bindings + preprocessImportPath unit tests. tsc
clean; full unit suite green (3 analyze worker-pool tests are pre-existing load
flakes); 1229 import/cross-file/resolver integration tests pass incl. the
wildcard-import languages (Go/Ruby/C++/Swift) that previously used synthesis.

* docs(ingestion): scrub stale references to deleted resolution-context machinery (RING4-2 #943)

* docs(ingestion): reword route resolver comment to clear acceptance grep gate (#943)

* fix(review): apply autofix feedback (RING4-2 #943)

Code-review autofixes from the multi-agent pass:
- delete orphaned dead code the deletion missed: swift.ts groupSwiftFilesByTarget
  + SwiftPackageConfig import (live copy is target-grouping.ts), import-resolvers
  EMPTY_INDEX export (no consumers after the importCtx reset was removed)
- scrub stale comments referencing deleted symbols (processImports,
  preprocessImportPath, moduleAliasMap, NamedImportMap/PackageMap, wildcard-synthesis)
  and fix a broken comment fragment in parse-impl.ts
- document the intentional global-resolution convergence for route controllers
  (the import-scoped tier was deleted with the resolver): confidence flattens
  0.9→0.5 but resolved edges stay at the 0.5 process-trace/community gate; only
  the narrow imported-controller-with-unresolved-method guessed edge crosses it
- add an overloaded-method characterization case pinning lookupExactAll[0]

* style(ingestion): prettier-format parse-impl unwind + route characterization test (#943)

* refactor(ingestion): address tri-review findings (RING4-2 #943)

From the PR #2033 tri-review (Codex + CE lanes):
- delete the now-dead importSemantics provider field + ImportSemantics type
  (wildcard-synthesis.ts was its sole consumer; zero readers remain) across
  language-provider.ts + 7 providers + DEFAULTS
- correct the processRoutesFromExtracted JSDoc: the import-disambiguated
  controller skip is STRICTER than the legacy global-tier guard (the legacy
  import-scoped tier resolved aliased / same-short-name controllers and emitted
  the edge); document the aliased-import missed-edge case explicitly
- add an aliased-controller characterization test pinning the documented
  global-resolution convergence (no edge for an aliased/unresolvable controller name)
- scrub stale parse-impl.ts docstrings/comments that still listed the removed
  import-resolution / wildcard-synthesis / heritage passes

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

* feat(ingestion): capture routes-file use/FQN map for Laravel controller resolution (#943)

Adds ExtractedRoute.controllerQualifiedName: the Laravel route extractor now
builds the routes file's `use`-import alias map (local→normalized dot-joined
FQN, via splitNamespaceUseDeclaration) and captures inline qualified ::class
references, threading the disambiguating FQN through every route. Normalized via
the shared normalizeQualifiedName so it matches the type registry's key shape
(issue #1982). Foundation for qualified-first route→controller resolution (U2).

* fix(ingestion): resolve Laravel route controllers qualified-first (#943)

processRoutesFromExtracted now resolves the controller via
model.types.lookupClassByQualifiedName(route.controllerQualifiedName) when the
extractor disambiguated it (aliased use / same-short-name / inline FQN), falling
back to the short-name lookupClassByName (which still skips on ambiguity). This
restores the route→controller CALLS edges the PR #2033 tri-review (Codex F1 +
ce-adversarial) found dropped, without re-adding the deleted per-file import map.
Method resolution, guessed-id, and confidence are unchanged. JSDoc rewritten to
qualified-first precedence; the aliased characterization test flips from no-edge
to edge; adds duplicated-name-disambiguated + stale-FQN-fallback cases.

* test(ingestion): end-to-end Laravel route→controller qualified resolution + PSR-4 disambiguation (#943)

Adds an integration test that parses real namespaced PHP controllers + a routes
file through the worker pipeline and asserts the route CALLS edges target the
correct namespaced controller — the authoritative gate the unit tests can't be
(hand-built models). It surfaced that PHP's statement-form `namespace X;`
leaves the structure-phase qualifiedName as the SHORT name, so
lookupClassByQualifiedName misses; resolveControllerByQualifiedName now adds a
PSR-4 file-path disambiguation (FQN namespace tail ↔ file directory tail) to
pick the right same-short-name controller. Forces the worker path
(workerThresholdsForTest) since route extraction is worker-only.

* style(ingestion): prettier-format Laravel route resolution changes (#943)

* test(ingestion): regenerate php-captures golden for the new php-laravel-routes fixture (#943)

* test(ingestion): move route fixture out of the php-* scope-capture corpus (#943)

The laravel route-resolution fixture lived under lang-resolution/php-laravel-routes,
which the php scope-capture golden + benchmark both glob (lang-resolution/php-*),
drifting their fingerprints. The fixture is for route resolution, not php
scope-capture parity, so rename it to lang-resolution/laravel-route-resolution
to decouple it. Reverts the golden's php-laravel-routes entries; bench
scope-capture --check passes (php back to baseline).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:51:28 +01:00
azizur100389 c4ee911463 fix(kotlin): detect default parameter arity (#2034)
* fix(kotlin): detect default parameter arity

* test(kotlin): rebaseline optional arity captures

* test(kotlin): cover default parameter boundaries
2026-06-04 17:28:04 +01:00
SparshandGergő Magyar 7cbc544299 fix(php): import decomposition, enum cases, anonymous class scope — F53,F54,F55 (#1931) (#1989)
* fix(php): import decomposition, enum cases, anonymous class scope — F53,F54,F55 (#1931)

* chore: fix unused imports, format, rebuild gitnexus-shared for macro type

* chore(bench): update PHP scope-capture baseline to CI-computed hash

* fix(php): reviewer fixes — grouped prefix, dead code removal, test precision

* feat: add F55 anonymous class pipeline test

* chore: fix format and benchmark baseline

* chore: regen PHP golden after F53/F54/F55 query changes

* chore: remove pipeline test, add grouped-prefix test, update fingerprint

* chore: remove unused beforeAll and path imports

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-04 12:57:38 +01:00
Gergő MagyarandCursor 938111ad45 fix(ci): stabilize gitleaks after #2024 (#2027)
* fix(ci): stabilize gitleaks after #2024 and clear history false positive

Fetch PR base/head SHAs before gitleaks-action so fork PRs do not fail with
ambiguous revision ranges. Add .gitleaks.toml allowlist for fake keys in
http-embedder tests, rename the redaction probe key, and point the README CI
badge at abhigyanpatwari/GitNexus.

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

* fix(ci): restore gitleaks default rules and narrow allowlist

Add [extend] useDefault = true so default secret rules run again. Replace
file-level allowlist with regexes for known fake embedding API keys.
Route PR SHAs through env vars in the gitleaks fetch step.

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

* Update README.md

* Update README.md

* Update README.md

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:28:59 +01:00
Gergő MagyarandClaude Opus 4.8 2aa78a60a7 refactor(ingestion): share a codec for __heritage__/__property__ markers (Ruby + Dart) (#1994) (#2007)
* refactor(ingestion): share a codec for __heritage__/__property__ markers (Ruby + Dart) (#1994)

The Ruby and Dart heritage/property pipelines encoded side-effect facts as ':'-delimited synthetic-import marker strings, hand-constructed and hand-parsed at ~8 sites with the field layout kept in agreement only by a comment — the fragility behind the #1981 edge-drop. Route every site through a single shared codec (utils/heritage-marker.ts: encodeMarker / decodeMarker / isHeritageMarker).

encodeMarker throws on a colon-bearing field so the silent-drop class becomes a loud failure; the ':' wire format is preserved byte-for-byte (ruby-captures-golden unchanged). Language-neutral — keyed only on the literal shared prefixes. Dart already single-sources its prefix and is heritage-only, so its import-target guard is left untouched (no invented __property__ path). Pure refactor: no new edges or behavior.

Verified: new codec unit test; ruby resolver + golden 155/155 (zero golden diff) and dart resolver 63/63 on registry-primary, both green on legacy; tsc + prettier clean.

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

* refactor(dart): single-source DART_HERITAGE_PREFIX from the shared codec (#1994)

Alias DART_HERITAGE_PREFIX to HERITAGE_MARKER_PREFIX (utils/heritage-marker.ts)
instead of re-declaring the '__heritage__:' literal, so the Dart import-target
heritage guard cannot desync from the codec's encode/decode. Value-identical;
gives the codec prefix a direct production consumer. Addresses the tri-review
nit on PR #2007.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:00:53 +01:00
Gergő MagyarandClaude Opus 4.8 560291ad6e fix(ingestion): qualify Ruby same-tail nested mixin modules + route IMPLEMENTS by scope (#1991) (#2006)
* fix(ingestion): qualify Ruby same-tail nested mixin modules + route IMPLEMENTS by scope (#1991)

A Ruby `module` maps to the Trait label but is not a typeDeclaration, so the structure phase never qualified its node id: two same-tail nested mixin modules (App::Loggable / Web::Loggable) collapsed onto one Trait:f.rb:Loggable node and the bare-name `include Loggable` cross-wired IMPLEMENTS (first-wins tail).

Structure phase: expose buildQualifiedName as a `qualifyScopeName` ClassExtractor hook and thread it for Trait nodes in parsing-processor + parse-worker (lockstep), so a module node keys by its qualified scope path (App.Loggable). Not Option A — `Trait` is not in CLASS_LIKE_LABELS and the qualified-id selection gates it out; qualifyScopeName bypasses the typeDeclaration gate that makes extractQualifiedName bail on modules. getQualifiedOwnerName also falls back to qualifyScopeName so methods inside a nested module own through the same qualified Trait id (no dangling HAS_METHOD).

Resolution: emitRubyMixinEdges resolves a bare mixin reference lexically by the including class's enclosing scope (`App::S` + `Loggable` -> `App::Loggable`), and the simple-tail fallback is now delete-on-collision (refuse to guess on a same-tail tie) instead of first-wins.

New single-file fixture + tests: two distinct Trait nodes, S IMPLEMENTS App.Loggable only, T IMPLEMENTS Web.Loggable only, no dangling HAS_METHOD; both resolver legs + worker path. Module->Trait preserved; Trait NOT added to CLASS_LIKE_LABELS. ruby-captures-golden regenerated additively.

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

* refactor(ingestion): single-source the Ruby Trait scope-label predicate; regen ruby bench baseline (#1991)

F5 follow-up to #1991: replace the four hardcoded `nodeLabel === 'Trait'` checks
(two each in the sequential parsing-processor.ts and worker parse-worker.ts
definition paths) with a single isQualifiableScopeLabel() in ast-helpers.ts so the
lockstep paths can't drift. Value-identical predicate — no behavior change.

Also regenerate the ruby scope-capture bench baseline: #1991 added the
ruby-nested-mixin-tail-collision fixture (and updated the ruby captures-golden),
but the bench baseline was never regenerated, so the order-independent fingerprint
drifts (bf6b13a -> f0d9b4c6, fixture_count 85 -> 86). Pure fixture-corpus drift.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:38:41 +01:00
Gergő MagyarandClaude Opus 4.8 083aedbc41 refactor(ingestion): delete legacy call-resolution DAG + heritage processor (RING4-1, #942) (#2023)
* refactor(ingestion): delete legacy call-resolution DAG + heritage processor (#942)

RING4-1: all 16 production languages (incl. Vue #940) are registry-primary, so
the legacy resolution legs only ran under the now-removed CI parity gate. Calls
and inheritance now resolve exclusively through scope-resolution
(Registry.lookup, preEmitInheritanceEdges, emitHeritageEdges, buildMro →
MethodDispatchIndex).

Removed:
- Call-resolution DAG: call-processor.ts legacy body (processCalls,
  processCallsFromExtracted, resolveCallTarget + all resolver/dispatch/chain
  helpers), model/resolve.ts MRO-via-HeritageMap, model/heritage-map.ts,
  type-env DAG types; inferImplicitReceiver/selectDispatch LanguageProvider
  hooks + Ruby impls; DispatchDecision/ImplicitReceiverOverride/ReceiverEnriched.
- Legacy heritage path: heritage-processor.ts, heritage-types.ts,
  heritage-extractors/, @heritage.* tree-sitter queries, heritageExtractor/
  heritageDefaultEdge/interfaceNamePattern wiring, worker + parse-impl heritage
  passes (parse-worker/parsing-processor lockstep), cross-file-impl DAG pass.
- Scope-parity infrastructure entirely (no legacy↔registry parity left to run):
  scripts/run-parity.ts, scripts/ci-list-migrated-languages.ts,
  ci-scope-parity.yml, test:parity, and the scope-parity ci.yml gate. Resolver
  integration tests still run via the normal tests job.

Kept (shared infra, NOT call-DAG-only): type-env.ts buildTypeEnv (field
extraction / structure phase / embeddings), model/resolve.ts c3Linearize +
gatherAncestors (mro-processor mroPhase), route/fetch/exported-type-map helpers
in call-processor.ts, preEmitInheritanceEdges (legacy-edge dedup simplified).

Acceptance: grep for resolveCallTarget/inferImplicitReceiver/selectDispatch/
buildHeritageMap/HeritageMap/processHeritage/heritageExtractor/@heritage. is zero
across src + test. tsc clean (both packages); resolver integration suite green
(bit-compatible EXTENDS/IMPLEMENTS/CALLS); scope-capture fingerprints unchanged
(python re-baselined: removed redundant ignored captures). ARCHITECTURE.md
updated to scope-resolution-only.

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

* fix(review): apply autofix feedback (#942)

ce-code-review autofix pass on the RING4-1 deletion:
- parse-cache.ts: bump SCHEMA_BUMP 2→3 — ParseWorkerResult lost its `heritage`
  field, so stale on-disk caches must invalidate (prevents a rollback replaying
  a heritage-less cache into legacy code) [api-contract P2].
- parse-impl.ts: drop 3 now-unused type imports (ExtractedCall,
  ExtractedAssignment, FileConstructorBindings) left by the deferred-block
  removal — would fail the eslint CI gate [correctness+maintainability P1].
- AGENTS.md / CLAUDE.md / scope-resolver.ts contract doc: fix stale pointers to
  the deleted "§ Call-Resolution DAG" section + removed hooks; preserve the
  language-neutrality rule [project-standards P1].
- registry-primary-flag.ts / cross-file.ts / parse-impl.ts: refresh stale
  comments referencing deleted symbols (legacy DAG, runCrossFileBindingPropagation).

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

* refactor(ingestion): remove the vestigial isRegistryPrimary flag (#942)

With the legacy call-resolution DAG deleted, the per-language
`REGISTRY_PRIMARY_<LANG>` / `isRegistryPrimary` / `MIGRATED_LANGUAGES` flag had
only one meaningful state — every production language resolves via
scope-resolution — and an explicit `=0` override could only *disable*
resolution with no fallback (a footgun the review flagged). Removing it.

- Delete `registry-primary-flag.ts` and the now-dead `shadow-harness.ts`
  (legacy↔registry shadow-parity tool) + its test.
- Collapse the three flag gates to their behavior-preserving outcome
  (`SCOPE_RESOLVERS == MIGRATED_LANGUAGES`, so this is a no-op):
  - scope-resolution phase now runs for every registered `SCOPE_RESOLVERS`
    entry (was `∩ MIGRATED_LANGUAGES`).
  - import-processor `addImportGraphEdge` + parse-impl `shouldAccumulate`:
    the legacy emit/accumulate paths were already inert for migrated
    languages (scope-resolution owns IMPORTS via the imports-to-edges bridge);
    drop the flag term.
- Collapse flag-branching tests to the scope-resolution path and delete the
  csharp legacy-`=0`-leg describe blocks; remove the ruby/rust-scope env-forcing
  hooks (no-ops now).
- Refresh docs/comments (ARCHITECTURE.md "one registration", scope-resolver
  cookbook, phase deps) — adding a language is now a single `SCOPE_RESOLVERS`
  registration.

Verified: tsc clean (both packages); resolver integration tests green
(747 assertions across cobol/csharp/ruby/rust/typescript/go, IMPORTS edges
intact); grep for the flag symbols is zero across src + test.

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

* style(format): prettier formatting on #942 changes

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

* fix(ci): drop legacy heritage-capture tests + re-baseline scope-capture fingerprints (#942)

Two CI failures from the #942 cleanup, surfaced by the tri-review + CI:

- tree-sitter-languages.test.ts: two tests asserted `@heritage.*` captures
  (Rust trait-impl, Dart extends/implements/with) that this PR removed. The
  acceptance grep used `@heritage\.` (with `@`); these reference the runtime
  capture name `heritage.trait` (no `@`), so they slipped the earlier sweep.
  Inheritance is now covered by the resolver integration suite. (fixed macos-latest)

- Re-baselined the scope-capture bench fingerprints for csharp/rust/ruby/java/
  javascript/kotlin (baselines.json) + python (python-scope/baseline-fingerprint.txt).
  The earlier test-cleanup reworded comments inside the lang-resolution fixture
  files (Shapes.cs, child.rs, derived.rb, IA.java/Plain.java, Service.js, F.kt,
  app.py) to scrub deleted-symbol references for the acceptance grep; those are
  the bench corpus, so capture node positions shifted. Capture LOGIC is
  unchanged — verified `--check` passes for all 14 langs + python. (fixed benchmarks)

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

* docs/chore: scrub remaining REGISTRY_PRIMARY + deleted-symbol references (#942)

Tri-review P3 follow-ups (verified):
- TESTING.md: rewrite the "Scope-resolution parity" section — the legacy
  dual-leg (REGISTRY_PRIMARY_<LANG>=0/1) and `npm run test:parity` no longer
  exist; resolver tests run once on the sole scope-resolution path in the
  normal tests job.
- scripts/bench-scope-resolution.ts: drop the inert `REGISTRY_PRIMARY_PYTHON=1`
  env set + usage hint (the flag is gone).
- ruby/scope-resolver.ts, php/captures.ts: re-point doc-comments off the
  deleted heritage-map.ts / heritage-processor.ts to the current behavior.

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

* fix(ci): prettier format + regenerate scope-capture goldens (#942)

Two more CI failures, same root cause as the bench re-baseline (the
test-cleanup reworded comments in lang-resolution bench/golden-corpus fixtures):

- quality/format: prettier on tree-sitter-languages.test.ts (blank line left by
  the deleted heritage-capture tests) + TESTING.md (the rewritten section).
- tests/ubuntu/coverage: `csharp-captures-golden` (and python/ruby/rust) drifted
  because the edited fixtures feed the per-language capture-golden snapshots too
  (not just the bench). Regenerated via UPDATE_GOLDEN=1. Verified safe: only the
  edited-fixture entries changed; csharp `captureGroups` unchanged (38) — digest
  shifted from comment-position only; capture LOGIC untouched. 1168 scope-
  resolution tests pass.

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

* test(resolvers): drop createResolverParityIt wrapper, use vitest it directly

The parity-aware `it` wrapper became a no-op when #942 removed the legacy
call-resolution DAG (it just returned vitest's `it`). Remove it entirely so
the resolver tests call vitest's `it` directly instead of shadowing it with a
local `const it` (or `pit`/`rustParityIt`):

- helpers.ts: delete createResolverParityIt + its now-unused vitestIt import
  and VitestIt type.
- 16 files: drop `const it = createResolverParityIt('x')` and import `it`
  from vitest instead.
- ruby.test.ts (pit) + rust.test.ts (rustParityIt): rename calls to `it`.
- Scrub every comment that described the removed wrapper / dual-mode parity
  skip / legacy_skip gate (vue-scope, js/ts/dart/php/python headers, rust x2,
  cpp, swift x4, rust-coverage). Genuine test rationale is kept; only the
  vestigial two-leg framing is dropped. Accurate "legacy DAG (removed in
  #942)" historical notes are retained.

No fixtures touched (no bench/golden re-baseline). tsc clean; rust+ruby
resolver suites green (323 tests, incl. #1992 worker-path parity after a
local dist build).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:07:37 +01:00
Gergő MagyarandClaude Opus 4.8 9f3bcee7fc fix(cpp): resolve cross-namespace same-tail inheritance bases bridge-held (#1993) (#2005)
* fix(cpp): resolve cross-namespace same-tail inheritance bases bridge-held (#1993)

PR #1981's bridge fixed within-namespace same-tail heritage (NS::A::Inner vs NS::B::Inner). The residual: a cross-namespace same-tail base (NS1::A::Inner vs NS2::A::Inner) both key the namespace-omitted `A.Inner` in the qualifiedNames index, so resolveQualifiedInheritanceBase couldn't pick a winner and the deriving classes cross-wired (DB's EXTENDS bound to NS1's A::Inner).

Fixed bridge-held via the existing `namespacePrefix` sidecar — no qualifiedName invariant flip, no resolution-index re-keying: (1) tagNamespacePrefixes also tags defs declared directly in a namespace (the deriving NS1::DA), composed identically to the class-nested path; (2) resolveQualifiedInheritanceBase breaks a same-tail tie by preferring the candidate whose namespacePrefix matches the deriving class's. Two-phase lookup, UDC, brace-init, file-local linkage untouched (def.qualifiedName + index keys unchanged).

New cpp-cross-namespace-same-tail fixture + registry-primary test (in the cpp parity expected-failures). Verified: cpp suite 287/287 primary, 209 + 78 skips legacy — no regression; tsc + prettier clean.

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

* test(cpp): worker-path parity for #1993 cross-namespace tie-break + correct narrative

Add the missing parse-worker.ts parity describe for the #1993 cross-namespace
same-tail heritage tie-break, mirroring the #1982/#1995 worker siblings
(workerThresholdsForTest minFiles:1/minBytes:1, workerPoolSize:2, usedWorkerPool
guard, and the same NS1.DA→NS1.A.Inner / NS2.DB→NS2.A.Inner base assertions), and
register both worker test names in LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES['cpp']
(registry-primary-only, like the sequential entry). Closes the DoD sequential≡worker
gap flagged in the tri-review of PR #2005.

Also correct the fixture/test narrative: the pre-fix failure is a CROSS-WIRE (DB's
EXTENDS binds NS1::A::Inner via the refuse-on-tie scope-walk fallback), not a silent
miss — the empirical pre-fix run shows the edge exists but points at the wrong target.

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

* refactor(scope-resolution): type the namespacePrefix sidecar; regen cpp bench baseline (#1993)

F4 follow-up to #1993: declare `namespacePrefix?: string` on SymbolDefinition
(gitnexus-shared) and drop the six `as { namespacePrefix?: string }` casts in
walkers.ts / graph-bridge/ids.ts that #1993 introduced. Pure type-level — the `as`
assertions erase at compile time, runtime is byte-identical, and the field stays a
sidecar (no graph-node identity; the qualifiedName-keyed index is untouched).

Also regenerate the cpp scope-capture bench baseline: rebased onto main (now
carrying #1995's cpp fixtures), #1993 adds cpp-cross-namespace-same-tail, growing
the cpp-* corpus 272->273 and drifting the fingerprint d63ded6->6d6207ae. Pure
fixture-corpus drift — no scope-extractor change.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:34:56 +01:00
Gergő MagyarandClaude Opus 4.8 e316222cd5 fix(cpp): distinct nodes for union- and anonymous-namespace-nested same-tail types (#1995) (#2004)
* fix(cpp): qualify types nested in a named union by their union scope (#1995)

`union_specifier` was missing from cppClassConfig.ancestorScopeNodeTypes, so a struct nested in `union U1` and one in `union U2` both qualified to the bare `Inner` and merged onto one Struct:...:Inner node — from_u1/from_u2 cross-wired (invisible to findDanglingEdges). Adding `union_specifier` lets buildQualifiedName pick up the named union's `name` segment, materializing distinct `U1.Inner` / `U2.Inner` nodes. Anonymous unions have no `name` child and correctly contribute nothing (members inject into the enclosing scope); the separate C config is untouched. New fixture + positive-identity tests (sequential + worker, both legs).

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

* fix(cpp): distinct nodes for anonymous-namespace-nested same-tail types (#1995)

An anonymous `namespace { }` is a namespace_definition with no `name` child, so the scope walker dropped it (empty segment) and two `namespace { struct Inner {} }` blocks in one TU collapsed onto a single `Inner` node — from_anon_a/from_anon_b cross-wired. A C++ `extractScopeSegments` override (the first consumer of the existing config hook) gives each anonymous namespace a deterministic per-block discriminator from its start byte, keeping the nested types distinct. Named scopes (incl. `inline namespace`) and anonymous unions are unaffected. Deterministic across the sequential and worker full-file parses. New fixture + tests assert node DISTINCTNESS (count==2 / distinct owners), not the non-portable discriminator value.

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

* test(cpp): regenerate cpp scope-capture bench baseline for #1995 fixtures

Rebased onto main (which now carries #1992 + its rust baseline). #1995 adds the
cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures, growing
the cpp-* corpus 270->272 and drifting the order-independent fingerprint
(538e8be -> d63ded6). Pure fixture-corpus drift — no scope-extractor change;
existing fixtures' captures byte-identical. (cpp has no captures-golden gate, so
only the bench baseline needs regenerating.)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:58:26 +01:00
16014657c8 docs: clarify local development setup in CONTRIBUTING (#2024)
* docs: add CODEOWNERS (#2)

* docs: add CI badge to README

* docs: add CODEOWNERS file

---------

Co-authored-by: Typo Fix Bot <fix@example.com>

* docs: add CI badge to README (#1)

Co-authored-by: Typo Fix Bot <fix@example.com>

* docs: clarify local development setup in CONTRIBUTING

* Update CODEOWNERS

---------

Co-authored-by: Typo Fix Bot <fix@example.com>
Co-authored-by: Arvuno <arvuno@nous.local>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-04 09:13:27 +01:00
Gergő MagyarandClaude Opus 4.8 c11f50a06e fix(ingestion): own generic Rust inherent-impl methods through the mod-qualified Impl node (#1992) (#2003)
* fix(ingestion): own generic Rust inherent-impl methods through the mod-qualified Impl node (#1992)

A generic inherent-impl target (`impl<T> Inner<T>`) is a `generic_type` node, which the inherent-impl owner walk (findEnclosingClassInfo) did not match — so the walk returned null and the method got `File -> DEFINES` with NO HAS_METHOD edge (orphaned, and invisible to findDanglingEdges). The Impl node was already correctly mod-qualified (the @name capture drills into the inner type_identifier, tree-sitter-queries.ts), so this is an owner-walk-only fix: drill into the generic base and mirror the node gate so the owner id == the node id byte-for-byte. A scoped-generic target (`impl<T> a::Inner<T>`) materializes no Impl node and is left orphaned (deferred) rather than minting a phantom owner.

The owner walk is shared by the sequential and worker paths. New fixture + tests assert positive HAS_METHOD ownership through distinct `a.Inner` / `b.Inner` nodes on both resolver legs and the worker path, plus a negative scoped-generic guard. rust-captures-golden regenerated additively for the new fixture.

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

* fix(ingestion): qualify className for same-tail Rust generic impls + regen rust bench baseline (#1992)

F3 follow-up to #1992: two same-tail generic inherent impls under sibling mods
that ALSO share a method name (`mod a { impl Inner { fn m } }` +
`mod b { impl Inner { fn m } }`) keyed the method node id `${className}.${name}`
with the bare tail (`Inner.m`) and collapsed onto one Function node (graph addNode
is first-write-wins), silently dropping the second. The owner Impl `classId` was
already mod-qualified, masking the collision behind distinct HAS_METHOD sources.
Qualify `className` (`a.Inner` / `b.Inner`) in the bare inherent-impl arm so the
node id inherits the mod scope; symmetric with the call-resolution fallback, and
the HAS_METHOD owner anchors on the unchanged qualified classId. New
same-method-name fixture + sequential & worker-parity tests; holds on both legs.

Also regenerate the rust scope-capture bench baseline: the new
rust-nested-tail-collision-generic (#1992) + rust-generic-impl-same-method-name
(F3) fixtures grow the rust-* corpus, so the order-independent fingerprint drifts
(56ffc1c0 -> b00aea0f, fixture_count 127 -> 129). Pure fixture-corpus drift — no
scope-extractor change; existing fixtures' captures byte-identical.

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

* test(rust): regenerate rust-captures golden for the F3 same-method-name fixture (#1992)

The rust-* scope-capture corpus is fingerprinted by TWO gates: the bench baseline
(bench/scope-capture/baselines.json, already updated) and the rust-captures-golden
unit test (test/fixtures/rust-captures-golden/expected-captures.json). Adding the
F3 fixture rust-generic-impl-same-method-name grew the corpus 128->129 entries, so
the committed golden drifted too. Regenerated additively (UPDATE_GOLDEN=1) — only
the new fixture's entry is added; existing fixtures' captures are byte-identical.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 08:55:09 +01:00
dependabot[bot] ff5cb0c0ff chore(deps)(deps): bump langchain from 1.3.5 to 1.4.2 in /gitnexus-web (#2015)
Bumps [langchain](https://github.com/langchain-ai/langchainjs) from 1.3.5 to 1.4.2.
- [Release notes](https://github.com/langchain-ai/langchainjs/releases)
- [Commits](https://github.com/langchain-ai/langchainjs/compare/@langchain/aws@1.3.5...langchain@1.4.2)

---
updated-dependencies:
- dependency-name: langchain
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:04:09 +01:00
dependabot[bot] 9ed9aa3489 chore(deps)(deps-dev): bump @vercel/node in /gitnexus-web (#2009)
Bumps [@vercel/node](https://github.com/vercel/vercel/tree/HEAD/packages/node) from 5.8.2 to 5.8.8.
- [Release notes](https://github.com/vercel/vercel/releases)
- [Changelog](https://github.com/vercel/vercel/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/vercel/vercel/commits/@vercel/node@5.8.8/packages/node)

---
updated-dependencies:
- dependency-name: "@vercel/node"
  dependency-version: 5.8.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:03:57 +01:00
dependabot[bot] 2877cffb27 chore(deps)(deps): bump lucide-react in /gitnexus-web (#2012)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.14.0 to 1.16.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.16.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:03:45 +01:00
dependabot[bot] 64d6a13439 chore(deps)(deps): bump i18next from 26.2.0 to 26.3.0 in /gitnexus-web (#2011)
Bumps [i18next](https://github.com/i18next/i18next) from 26.2.0 to 26.3.0.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.2.0...v26.3.0)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:03:34 +01:00
dependabot[bot] 3fef36572a chore(deps)(deps): bump dompurify from 3.4.3 to 3.4.7 in /gitnexus-web (#2013)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.3 to 3.4.7.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.3...3.4.7)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:03:21 +01:00
dependabot[bot] ad36a86ab0 chore(deps): bump docker/login-action from 4.1.0 to 4.2.0 (#2020)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/4907a6ddec9925e35a0a9e82d7399ccc52663121...650006c6eb7dba73a995cc03b0b2d7f5ca915bee)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:03:07 +01:00
dependabot[bot] 7982eaea12 chore(deps): bump docker/metadata-action from 6.0.0 to 6.1.0 (#2018)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/030e881283bb7a6894de51c315a6bfe6a94e05cf...80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:57 +01:00
dependabot[bot] 42ac44b838 chore(deps): bump docker/build-push-action from 7.1.0 to 7.2.0 (#2010)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.1.0 to 7.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/bcafcacb16a39f128d818304e6c9c0c18556b85f...f9f3042f7e2789586610d6e8b85c8f03e5195baf)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:44 +01:00
dependabot[bot] 617ca0d05e chore(deps): bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#2019)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd...d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:31 +01:00
dependabot[bot] 81b46518b2 chore(deps): bump github/codeql-action from 4.35.5 to 4.36.0 (#2017)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.5 to 4.36.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e0d7b8d25671d64c341c19c0152d693099fb5ba...7211b7c8077ea37d8641b6271f6a365a22a5fbfa)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:20 +01:00
dependabot[bot] 1bb5b31745 chore(deps): bump aiohttp in /eval in the uv group across 1 directory (#2008)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:02:09 +01:00
dependabot[bot] 76684b8d4b chore(deps)(deps-dev): bump tsx from 4.22.3 to 4.22.4 in /gitnexus (#2016)
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.22.3 to 4.22.4.
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.3...v4.22.4)

---
updated-dependencies:
- dependency-name: tsx
  dependency-version: 4.22.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 07:01:59 +01:00
Gergő MagyarandClaude Opus 4.8 8a9b13fc3b feat(cli): add .gitnexusrc config and --default-branch for analyze (#243) (#1996)
* feat(cli): add .gitnexusrc config and --default-branch for analyze (#243)

Let a repo preconfigure recurring `gitnexus analyze` options via a
project-local `.gitnexusrc` (JSON) plus a new `--default-branch` flag, so
projects on `develop`/`master` no longer get the generated regression
example rewritten to `base_ref: "main"` on every analyze run.

- New `cli/analyze-config.ts`: locate/parse/validate `.gitnexusrc` (flat +
  nested `analyze` form, alias mapping, fail-closed on unknown keys / bad
  types / hidden chars), merge with CLI (CLI overrides config), and resolve
  the default branch (CLI > config defaultBranch/branch > auto-detected
  origin/HEAD > "main").
- `getDefaultBranch()` in storage/git.ts (best-effort, local-only, no network).
- Thread `defaultBranch` through analyze -> run-analyze -> ai-context so the
  generated regression-compare example uses the configured branch,
  JSON-escaped; the --skills re-generation path uses the same branch.
- `skipContextFiles`/`skipAiContext` alias `skipAgentsMd` (block only, does
  not imply skipSkills); `indexOnly` stays the stronger "skip all injection".
- README + CLI help; unit tests for the config module and end-to-end wiring
  tests that fail if config is parsed but not threaded into analyze/context.

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

* fix(cli): harden .gitnexusrc against Markdown injection and stale base_ref (#243)

Addresses the tri-review findings on PR #1996.

- P1 (Markdown injection into generated AGENTS.md/CLAUDE.md): reject the
  backtick in validateBranchName (covers --default-branch, .gitnexusrc, and the
  origin/HEAD auto-detect via sanitizeDetectedBranch) and strip it at the
  ai-context sink (markdownSafeBranch); reject Markdown-significant chars
  (` * [ ] < >) in the config `name` (it lands in generated bold/code-spans),
  while still allowing `_ . - /`. Corrected the false "can't break the code
  span" comment.
- P2 (configured defaultBranch silently no-ops on an up-to-date repo): on the
  alreadyUpToDate fast path, surgically refresh only the `base_ref:` line in
  AGENTS.md/CLAUDE.md (refreshBaseRefLine), preserving the rest of the block
  incl. --skills community rows; no-op when unchanged.
- P3: gate the .gitnexusrc key lookup with Object.hasOwn so inherited keys
  (__proto__, constructor, …) hit the actionable "Unknown key" error.
- Cleanups: strip a leading UTF-8 BOM before JSON.parse; give --default-branch
  CLI validation its own `default-branch-invalid` recovery hint; drop the dead
  `options.defaultBranch` write and the now-redundant `options?.` chaining.
- Tests: backtick rejection + even-backtick generated output, 255-char branch
  bound, config `name` Markdown rejection, __proto__ → Unknown key, BOM,
  mergeAnalyzeOptions omits defaultBranch, willGenerateContext suppression, and
  the fast-path base_ref refresh.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 06:48:55 +01:00
evolutionandGergő Magyar 5cb00119e8 fix(go): normalize fixed-array parameter bindings (#1988)
* fix(go): normalize fixed-array parameter bindings

* fix(go): address parameter type review follow-ups

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-04 05:08:15 +01:00
Gergő MagyarandClaude Opus 4.8 a987c2c0e6 test(cli): make cli-e2e read-only + eval-server tests robust under load (#2000)
The query/cypher/impact stdout tests and the eval-server tests assumed mini-repo
had already been indexed by an earlier analyze test. That analyze test silently
tolerates a subprocess timeout (`if (result.status === null) return`), so under
parallel load (cli-e2e runs in the default integration project) the repo went
unregistered and every dependent test failed confusingly with "No indexed
repositories found" / exit 1.

- beforeAll now indexes mini-repo once into the isolated suite registry (retried
  a few times; re-analyze of an already-indexed repo is a cheap alreadyUpToDate
  no-op), removing the implicit cross-test ordering dependency.
- The four dependent describes get { retry: 2 } (Vitest 4 second-arg options) so
  a transient subprocess hiccup self-heals instead of failing the suite.

Genuine analyze/registration regressions are still caught loudly by the
dedicated analyze tests (which use isolated GITNEXUS_HOMEs). Full cli-e2e file:
34/34 pass locally.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 21:50:22 +01:00
c2b4ec6c31 feat(vue): migrate Vue SFC to scope-based resolution (RFC #909 Ring 3, closes #940) (#1950)
* feat(vue): migrate Vue SFC to scope-based resolution (RFC #909 Ring 3, closes #940)

Adds `vueScopeResolver` and wires Vue into the scope-resolution pipeline
(`SCOPE_RESOLVERS`, `MIGRATED_LANGUAGES`). Vue's `<script>` / `<script
setup>` blocks are TypeScript — `emitVueScopeCaptures` extracts the script
block via the existing `extractVueScript` utility and delegates to
`emitTsScopeCaptures`, keeping grammar identity consistent with the cached
tree the parse-worker already builds.

- `languages/vue/captures.ts`     — `emitVueScopeCaptures`
- `languages/vue/import-target.ts` — `makeVueResolveImportTarget` (TS
  resolver + tsconfig path-alias support; explicit `.vue` imports
  resolve via the exact-path branch)
- `languages/vue/scope-resolver.ts` — `vueScopeResolver`
- `languages/vue/index.ts`         — barrel + known-limitations doc

- `languages/vue.ts`                  — `emitScopeCaptures` hooked up
- `scope-resolution/pipeline/registry.ts` — Vue entry added
- `registry-primary-flag.ts`          — `SupportedLanguages.Vue` added
  to `MIGRATED_LANGUAGES` (production default → registry-primary)

- `vue-composition-api` — `<script setup lang="ts">`, defineProps /
  defineEmits macros, cross-file TS imports, computed refs
- `vue-options-api`     — `defineComponent({methods, computed, data})`,
  this-based method calls, imported utility calls
- `vue-cross-file`      — composable functions returning class instances,
  multi-level import chains, UserModel/PostModel method calls

- `fieldFallbackOnMethodLookup: true` — Options API `this.X()` calls may
  not resolve through the type-binding layer (no formal class); fallback
  catches common patterns via declared field names.
- `allowGlobalFreeCallFallback: false` — Vue uses explicit imports;
  workspace-wide unique-name fallback would produce spurious edges for
  built-ins (ref, reactive, defineProps, …).
- Template expression calls intentionally out of scope: component-
  reference CALLS edges are already emitted by the legacy template
  extractor. Remaining template gaps tracked in #1647.

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

* fix(vue): address P0/P1 review findings from #1950

## P0 #1 — missing scope-resolution hooks in vueProvider
`pass3CollectImports` early-returns when `interpretImport` is undefined,
producing zero IMPORTS and zero cross-file CALLS edges. Add the four
hooks to `vueProvider` in `vue.ts`:
  - `interpretImport: interpretTsImport`
  - `interpretTypeBinding: interpretTsTypeBinding`
  - `bindingScopeFor: tsBindingScopeFor`
  - `importOwningScope: tsImportOwningScope`
Also add `receiverBinding`, `mergeBindings`, `arityCompatibility`, and
`resolveImportTarget` to complete the scope-resolution contract.

## P0 #2 — template-component CALLS dropped when Vue is registry-primary
`isRegistryPrimary(Vue) → true` makes the main call-processor loop skip
Vue files entirely, silencing the inline `vue-template-component` CALLS
emitter at ≈L1506. Add a dedicated post-loop pass in `call-processor.ts`
that emits template-component CALLS for Vue files whenever Vue is
registry-primary. Update the stale `vue/index.ts` limitation comment to
reflect the new emit site.

## P1 #3 — worker-mode double-extraction → zero captures
In worker mode (≥15 files) the parse worker pre-extracts the `<script>`
block and passes `scriptContent` as `sourceText`. `emitVueScopeCaptures`
was calling `extractVueScript` a second time, getting null, and returning
`[]`. Fix: if extraction returns null and the content has no SFC block-
level markers (`<template`, `<style`), treat it as already-extracted
script text and delegate directly to `emitTsScopeCaptures`.

## Test assertion strictness
Replace all `toBeGreaterThanOrEqual(1)` assertions with exact `toBe(N)`
counts. IMPORTS counts reflect per-symbol scope-based edges (value imports
only; `import type` is not emitted as an IMPORTS edge). CALLS counts are
1 per single-call-site.

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

* feat(vue): template-derived edges + pipeline benchmark (#1950 review)

Addresses the reviewer's request for template edge attribution and a
performance benchmark.

## Template event-handler CALLS (`vue-template-callback`)
Add `extractTemplateEventHandlers` to `vue-sfc-extractor.ts`. Extracts
bare single-identifier handlers from `@event="methodName"` and
`v-on:event="methodName"` attributes. Inline expressions with arguments
or operators (`@click="toggle(item)"`) are intentionally excluded.

Wire into the dedicated registry-primary Vue template pass in
`call-processor.ts`. For each extracted handler name, `ctx.resolve`
finds the in-file Function/Method node and emits a CALLS edge with
`reason: 'vue-template-callback'`.

## Template attribute-binding ACCESSES (`vue-template-attribute`)
Add `extractTemplateAttributeBindings` to `vue-sfc-extractor.ts`.
Extracts bare single-identifier values from `:prop="varName"` and
`v-bind:prop="varName"` bindings. Member-access (`:key="post.id"`) and
literals are excluded by the identifier-boundary regex.

Wire into the same template pass. For each extracted variable, `ctx.resolve`
finds the in-file node and emits an ACCESSES edge with
`reason: 'vue-template-attribute'`.

## `vue/index.ts` limitations comment
Updated to accurately describe all three categories of template-derived
edges and explicitly document the complex-expression exclusions.

## Tests
Add 6 new assertions in `vue-scope.test.ts`:
- `@click="handleSave"` → CALLS `handleSave` (UserProfile.vue)
- `@select="onPostSelected"` → CALLS `onPostSelected` (App.vue composition)
- `@keyup.enter="addTodo"` → CALLS `addTodo` (TodoList.vue)
- `@loaded="onUserLoaded"` → CALLS `onUserLoaded` (App.vue cross-file)
- `:userId="currentUserId"` → ACCESSES `currentUserId` (App.vue composition)
- `:posts="allPosts"` → ACCESSES `allPosts` (App.vue composition)

Add `vue` entry to `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES` in
`helpers.ts` documenting which assertions are registry-primary-only
(IMPORTS cardinality, template-derived edges, `<script setup>` export).

## Benchmark
Add `vue-pipeline-benchmark.test.ts` (gated by `GITNEXUS_BENCH=1`).
Generates N-component synthetic repos (10 / 25 / 50 / 100) and asserts
that wall-clock and node counts scale sub-quadratically with component
count, guarding against O(n²) regressions in the template extraction
or scope-resolution passes.

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

* feat(vue): BINDS_EVENT_HANDLER/EMITS_EVENT edges via ScopeResolver hook

Per maintainer feedback on PR #1950:
- Do not edit call-processor.ts (will be removed when all languages migrate)
- Model Vue component-event system with dedicated edge types to avoid CALLS
  noise in deep component hierarchies (per contributor discussion)

Changes:
- gitnexus-shared: add BINDS_EVENT_HANDLER and EMITS_EVENT to RelationshipType
- vue-sfc-extractor: add extractComponentEventBindings, extractNativeElementEventHandlers,
  and extractScriptEmitCalls
- ScopeResolver contract: add optional emitPostResolutionEdges hook
- run.ts: wire emitPostResolutionEdges after emitImportEdges
- vue/scope-resolver: implement emitPostResolutionEdges emitting:
    1. CALLS (vue-template-component) — PascalCase component File refs
    2. CALLS (vue-template-callback) — @event on native HTML elements
    3. BINDS_EVENT_HANDLER (vue-event: @name) — @event on component elements;
       source = handler fn in parent, target = child component File (not CALLS)
    4. EMITS_EVENT (vue-emit: name) — emit() calls; self-loop on component File,
       joinable with BINDS_EVENT_HANDLER via Cypher for impact tracing
    5. ACCESSES (vue-template-attribute) — :prop="var" bindings
- call-processor.ts: revert dedicated Vue post-loop pass; moved to scope resolver
- Tests and parity expected-failures updated accordingly

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

* fix(vue): close review gaps in scope/parity extraction

Resolve the new PR #1950 review findings by widening Vue scope context to include TS/JS import closures, fixing BINDS_EVENT_HANDLER endpoint assertions, hardening emit/event extraction to avoid comment/property false positives, supporting kebab-case component tags, and ensuring parity runs include vue-scope suites.

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

* fix(vue): address second review round — regex safety, emit coverage, arch

Closes items raised in the Jun 2 review comment on PR #1950.

Correctness fixes:
- ReDoS mitigation: bound attribute-capture spans to [^>]{0,512}? in all
  three template tag regexes to prevent pathological backtracking.
- Kebab-case misclassified as native: added (?![A-Za-z0-9-]) negative
  lookahead to NATIVE_TAG_RE so <post-list> is no longer split as native
  tag `post` with attrs `-list ...`.
- Hyphenated event names dropped: widened TAG_EVENT_RE from [\w:.]+ to
  [\w:.-]+ so @user-loaded and @update:model-value are captured.
- this.$emit silently dropped: collectBareEmitEventNames now allows
  this.$emit(...) by looking back past the '.' to verify preceding token
  is exactly `this`; socket.emit etc. remain blocked.
- Event names with colon rejected: extended validator to accept
  update:modelValue and update:model-value patterns.

Architecture fix:
- Moved collectVueScopeFilePaths out of shared phase.ts into a new
  collectScopeContextPaths optional hook on ScopeResolver, keeping shared
  pipeline code language-agnostic. vueScopeResolver implements the hook.
- Fixed memory leak: preExtractedByPath cleanup now iterates filePaths
  (all context files) not just primaryFilePaths (only .vue files).

Cleanup:
- Removed unused extractTemplateEventHandlers and duplicate EVENT_HANDLER_RE.
- Fixed skipped comment numbers in emitPostResolutionEdges (1,2,4,5,6 -> 1-6).
- Updated vue/index.ts: four categories -> five (added EMITS_EVENT).
- Fixed gitnexus-shared EMITS_EVENT JSDoc to reflect File->File reality.

Tests: 7 new unit tests covering hyphenated events, this.$emit, kebab-case
native-tag exclusion, and update:modelValue event name validation.

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

* fix(vue): eliminate double file-read and per-file template re-scans

Two performance fixes from the self-review pass:

1. **No more double read of .vue files in phase.ts**: primary files were
   previously read once for `collectScopeContextPaths` (via
   `entryFileContents`) and again in the blanket `readFileContents(filePaths)`
   call. Now the primary-file map is passed directly and only the extra
   context files (TS/JS import closure) require a second I/O round-trip.

2. **Single template parse per .vue file in emitPostResolutionEdges**:
   previously each of the five extractor functions (components, native
   handlers, component event bindings, emit calls, attribute bindings) ran
   `TEMPLATE_RE.exec(content)` independently — five full-file scans per
   `.vue` file. Replaced with a new `extractVueTemplateEdgeData` batching
   helper that parses the template and script blocks once and feeds all five
   extractors from the pre-extracted content. emitPostResolutionEdges now
   calls a single function and destructures the results.

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

* fix(parity): exclude TypeScript HOC/HOF/JSX scope-resolver tests from legacy DAG parity gate

Three test files introduced in prior PRs exercise scope-resolver-only
correctness wins: HOC-wrapped const declarations, HOF-callback caller
attribution, and JSX-as-call CALLS edges. The parity runner's
${slug}-*.test.ts glob now picks them up, causing typescript [legacy]
failures in CI.

Fix: convert each file to use createResolverParityIt('typescript') and
register all 26 legacy-failing test names in
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.typescript with explanatory
comments. Legacy mode: 11+11+4 tests skipped, zero failures.
Registry-primary mode: all 37 tests pass as before.

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

* chore(test): remove registry-primary-flag unit tests after migration complete

All languages are now in MIGRATED_LANGUAGES; the per-language flip
tests are no longer needed. Addresses PR #1950 review feedback.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-03 21:48:38 +01:00
dependabot[bot]andGergő Magyar 226bd27cd0 chore(deps)(deps): bump lru-cache from 11.5.0 to 11.5.1 in /gitnexus (#1986)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.5.0 to 11.5.1.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.5.0...v11.5.1)

---
updated-dependencies:
- dependency-name: lru-cache
  dependency-version: 11.5.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-03 17:44:50 +01:00
Gergő MagyarandClaude Opus 4.8 c60ad9f7ab fix(ingestion): fully-qualified nested-type identity for C++/Ruby — structure (#1978) + resolution (#1982) (#1981)
* fix(ingestion): qualify nested-type node identity for C++/Ruby (#1978)

Nested types sharing a tail name in one file — C++ `Outer::Inner` vs
`Other::Inner`, Ruby `Outer::Inner` vs `Other::Inner` modules — silently merged
into a single graph node keyed by the simple tail (`Struct:file:Inner`),
cross-wiring their methods/properties onto one owner.

Key class-like type nodes (Class/Struct/Interface/Enum/Record) by their
normalized fully-qualified path (`Struct:file:Outer.Inner`) instead of the
simple name. Gated per-language by a new `qualifiedNodeId` config flag
(default false → byte-identical for every other language); enabled here for
C++ and Ruby.

- class-types.ts / generic.ts: `qualifiedNodeId` flag on ClassExtractor + config
- ast-helpers.ts: findEnclosingClassInfo gains an optional getQualifiedOwnerName
  hook + EnclosingClassInfo.qualifiedClassId, so member-owner edges resolve to
  the qualified class node id (owner id == node id by construction)
- parsing-processor.ts + parse-worker.ts: flag-gated qualified node-id + owner
  edges on both the sequential and worker parse paths (incl. routed properties)
- call-processor.ts: same qualifier in the routed-property pre-pass (lockstep
  with the worker `kind === 'properties'` block)
- configs/c-cpp.ts, configs/ruby.ts: qualifiedNodeId: true

Method/Property node ids stay simple-qualified; only type nodes get the
qualified id.

Deferred to a resolution-side follow-up: Ruby SAME-TAIL routed-property/mixin
owner identity under registry-primary (`emitRubyMixinEdges` keys owners by the
simple tail name, last-wins); and Rust inherent-impl methods (impl_item is not
a typeDeclaration — its #1978 test is describe.skip).

Tests: same-tail collision fixtures + #1978 resolver tests for C++/Ruby
(positive owner identity, R7), a worker-path parity block, and an unambiguous
nested attr_accessor case; the C++ #1975 out-of-line test updated to assert
qualified-id distinctness (forward-decl + out-of-line now unify). Verified
green on both parity legs, the worker path, and tsc.

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

* test(ingestion): scope #1978 resolver tests to registry-primary leg; fix lint

- helpers.ts: exclude the new #1978 C++/Ruby resolver tests from the legacy
  parity leg (LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES). They PASS on legacy
  too — the fix lives in the SHARED structure phase, not the legacy resolution
  path — so this is a deliberate registry-primary-only scoping (not a legacy
  gap), keeping the legacy path untouched and uncoupled from the new
  node-identity behavior.
- rust.test.ts: drop the `eslint-disable vitest/no-disabled-tests` directive.
  That rule isn't configured in this repo, so eslint errored "Definition for
  rule 'vitest/no-disabled-tests' was not found" and failed `quality / lint`.
  The describe.skip needs no disable directive.

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

* fix(test): satisfy CI for the new #1978 fixtures (format + golden + fingerprint)

Adding the {cpp,ruby,rust}-nested-tail-collision fixtures changed the
lang-resolution corpus, which the scope-capture golden snapshots and the
fingerprint baselines gate on. These are pure fixture-corpus additions —
#1978 does not touch the scope-capture phase (captures.ts / emit*ScopeCaptures
are unchanged). Verified: the regenerated ruby/rust golden diffs are
additive-only (no existing fixture's capture digest changed), so the cpp/ruby/
rust fingerprint drift is solely the new fixtures.

- prettier --write test/integration/resolvers/{ruby,rust}.test.ts
- regenerate ruby/rust captures-golden snapshots (UPDATE_GOLDEN=1; +1 fixture each)
- rebaseline cpp/ruby/rust scope-capture fingerprints (bench/scope-capture/baselines.json)

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

* refactor(ingestion): extract shared qualified-name normalizer (#1982)

Move normalizeQualifiedName/splitQualifiedName out of class-extractors/
generic.ts into utils/qualified-name.ts so the structure-phase
buildQualifiedName, the scope-resolution inheritance resolver, and the
per-language capture emitters can all key against ONE normalizer. A raw
'::' qualifier must normalize to the exact '.'-joined key the
QualifiedNameIndex already holds, or the qualified lookup silently misses
(the #1982 resolution-side foundation). Pure relocation — byte-identical
function bodies; tsc clean; existing C++ nested-collision tests green.

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

* fix(ingestion): resolve same-tail C++ nested-type heritage to the correct qualified node (#1982)

Registry-primary C++ inheritance (preEmitInheritanceEdges -> resolveInheritanceBaseInScope)
resolved a same-tail nested base by its SIMPLE TAIL with first-wins, so
`struct DerivedB : Other::Inner` mis-resolved EXTENDS to Outer.Inner (the wrong
sibling; 0 dangling, so undetected). The namespace qualifier was discarded at the
C++ inheritance capture.

Fix (additive, qualified-first):
- ReferenceSite gains an optional `rawQualifiedName`; the C++ inheritance capture
  emits `@reference.qualified-name` (qualifier-preserving, template-stripped:
  Other::Inner, ns::Base<T> -> ns::Base) only when the base is qualified, registered
  as a sub-tag so it can't shadow the `@reference.inherits` anchor.
- resolveInheritanceBaseInScope resolves the qualifier against the full-path
  QualifiedNameIndex FIRST (which already carries Outer.Inner / Other.Inner keys from
  the structure phase), with progressive-prefix lookup for relative bases and
  refuse-on-tie, falling through to the existing simple-tail walk on miss — so
  unqualified bases and the single-candidate cross-file case are unchanged.

Registry-primary cpp.test.ts 278/278 (incl. worker-path: rawQualifiedName survives
worker serialization). Legacy leg unaffected (207 pass / 71 skip) — the new
resolution-side assertions are registry-primary-only via helpers.ts. tsc clean.

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

* fix(ingestion): resolve same-tail Ruby mixin/attr_accessor owners to the correct qualified node (#1982)

emitRubyMixinEdges keyed its owner map by the SIMPLE tail (def.qualifiedName
split-popped) with last-wins, and the __heritage__/__property__ markers carried
only the immediate owner name — so `module Outer; class Inner` and
`module Other; class Inner` collapsed onto one `Inner` key and cross-wired their
include/attr_accessor edges onto whichever Inner was processed last.

Fix (lockstep, full-qualified):
- ruby/captures.ts: build the marker owner from the FULL enclosing class/module
  chain (buildEnclosingQualifiedName walks all ancestors, normalizing the compact
  `class Outer::Inner` scope_resolution form via the shared splitQualifiedName) so
  the marker owner byte-matches the resolution def's qualifiedName.
- ruby/scope-resolver.ts: key graphIdByName by the full def.qualifiedName instead
  of the simple tail. Top-level owners/mixins are unchanged (full == simple).

Registry-primary ruby.test.ts 142/142 incl. a new worker-path block (the deferred
note's duplicate-edge concern: markers survive worker serialization, exactly one
HAS_PROPERTY per attr). Legacy leg unaffected (136 pass / 6 skip) — new assertions
registry-primary-only via helpers.ts. tsc clean.

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

* test(ingestion): rebaseline #1982 golden/fingerprint + lint/format sweep

Cross-cutting verification artifacts for the #1982 same-tail resolution fix:
- ruby capture golden regenerated: ONLY the ruby-nested-tail-collision fixture
  drifts (+10 capture groups from its new include/attr_accessor + the now
  full-qualified __heritage__/__property__ marker owner). All other ruby fixtures
  byte-identical (proves the owner-qualification is localized to nested owners).
- bench/scope-capture/baselines.json: rebaseline cpp + ruby fingerprints (the only
  two that drift; 12 other languages byte-identical). cpp = additive
  @reference.qualified-name capture; ruby = the localized owner change. Provenance
  notes record both. scaling linear (~1.0), 14/14 PASS.
- generic.ts: drop the now-unused normalizeQualifiedName import (lint error).
- walkers.ts / ruby.test.ts: prettier formatting.

Verified: cpp 278/278 + ruby 142/142 (registry-primary), both legacy legs clean
(skips registry-primary-only assertions), go/java/csharp 542 (cross-language
regression — the qualified-first branch is gated on rawQualifiedName, set only by
C++, so non-C++ inheritance resolution is unchanged). tsc + eslint(0 errors) + prettier clean.

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

* fix(ingestion): resolve nested Ruby mixin included by short name (#1982)

emitRubyMixinEdges keyed graphIdByName by the full def.qualifiedName on the
owner side, but the __heritage__ marker carries the mixin target as the bare
written name (arg.text). A nested mixin module included by its short name
(include Loggable where it is App::Loggable) missed the full-qn map and its
IMPLEMENTS edge was silently dropped (0 dangling, undetectable). The shipped
same-tail fixture used only top-level mixin modules, so CI stayed green.

Add a secondary simple-tail fallback map consulted only when the full-qn mixin
lookup misses; owner lookups stay full-qn so same-tail owner disambiguation is
preserved. Characterization test + fixture (registry-primary only); golden
regenerated additively.

Addresses PR #1981 review (4417182679) P1.

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

* fix(ingestion): normalize qualified Ruby mixin arg in heritage marker (#1982)

`include Outer::Mixin` embedded the raw `Outer::Mixin` into the ':'-delimited
__heritage__ marker, so the `::` collided with the field separator and
emitRubyMixinEdges mis-split it (className became empty), dropping the IMPLEMENTS
edge. Normalize the mixin arg via splitQualifiedName(...).join('.') before emit
so the marker carries the dotted form, which both parses correctly and matches
the mixin def's qualifiedName. Simple names are unchanged (no golden drift).

Addresses PR #1981 review (4417182679) secondary R2.

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

* fix(ingestion): resolve C++ same-tail nested heritage inside a namespace (#1982)

A namespace-nested C++ type's scope-model qualifiedName carried its enclosing
CLASS chain (A.Inner) but dropped the enclosing NAMESPACE, while the
structure-phase graph node is keyed by the full path (NS.A.Inner). resolveDefGraphId's
qualifiedKey therefore missed and fell back to simpleKey('Inner'), collapsing
same-tail nested bases across sibling namespace members — DB : B::Inner pointed
at NS.A.Inner. The shipped fixture was top-level only, so it could not catch this.

Fix without disturbing the qualifiedName-keyed resolution index (an earlier
attempt that rewrote qualifiedName regressed brace-init / UDC / two-phase
namespace resolution): tagNamespacePrefixes records each namespace-nested def's
enclosing-namespace prefix on a sidecar field, and resolveDefGraphId retries the
node lookup with the namespace-prefixed key before the simpleKey fallback. The
helper is language-agnostic (acts only on Namespace scopes) and opt-in — only the
C++ provider calls it. Namespaced fixture + sequential & worker tests
(registry-primary only). All 280 cpp resolver tests pass; tsc clean.

Addresses PR #1981 review (4417182679) P2.

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

* test(ingestion): worker-path parity for Ruby mixin IMPLEMENTS + C++ DerivedA (#1982)

The Ruby worker-path parity block asserted only attr_accessor (HAS_PROPERTY);
add an IMPLEMENTS assertion so a dropped/cross-wired mixin owner on the worker
path is caught (the __heritage__ marker owner must survive serialization). The
C++ worker heritage block asserted only DerivedB; add a DerivedA assertion with
a toHaveLength(1) duplicate guard. Registry-primary only.

Addresses PR #1981 review (4417182679) test-coverage gap.

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

* fix(ingestion): distinct Rust same-tail nested-mod inherent-impl ownership (#1982)

Rust methods live in `impl Inner` blocks, and findEnclosingClassInfo keyed the
inherent-impl owner by the target's RAW tail (`Impl:lib.rs:Inner`), so two
same-tail `impl Inner` blocks under different mods (mod outer / mod other)
collapsed onto ONE Impl node and their methods cross-wired. The shipped fixture
test for this was skipped/deferred.

Qualify an UNSCOPED inherent-impl target by its enclosing `mod_item` scope
(`outer.Inner`) in BOTH the owner walk (ast-helpers.qualifyRustImplTargetByModScope)
and the Impl-node materialization (parsing-processor + parse-worker, lockstep) so
the owner edge and node id agree byte-for-byte. Gated on the Impl label +
impl_item + an unscoped type_identifier target — Rust-impl-exclusive, so C++/Ruby
and the rust captures golden are untouched; a SCOPED `impl a::Inner` keeps its
full raw text (#1975, unchanged). The previously-skipped distinct-ownership test
is now active and passing; rust 170/170, cpp+ruby+golden 437/437, tsc clean.

Done in-PR at maintainer request (was deferred as a follow-up). Addresses PR #1981
review (4417182679) test-coverage gap R7.

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

* refactor(ingestion): single qualified-name normalizer + module-scoped Ruby PROPERTY_PREFIX (#1982)

Replace cpp/captures.ts's parallel normalizeCppNamespaceQName with the shared
normalizeQualifiedName (behaviorally equivalent for C++ qualified-identifier
inputs: '::'->'.' with leading/trailing-:: handling; no interior whitespace
reaches it). Promote Ruby's PROPERTY_PREFIX to module scope alongside
HERITAGE_PREFIX (was function-local — asymmetric with no behavioral effect).
Maintainability only; cpp+ruby resolver suites 428/428, tsc clean.

Addresses PR #1981 review (4417182679) maintainability item.

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

* perf+fix(ingestion): single enclosing-class walk + root-anchored base guard (#1982)

U7 (perf): preEmitInheritanceEdges resolved the deriving class AND
resolveQualifiedInheritanceBase re-walked findEnclosingClassDef for the same
site. Resolve callerClass once and thread it into resolveInheritanceBaseInScope
-> resolveQualifiedInheritanceBase -> enclosingScopeSegments, so the enclosing
class is walked once per qualified site. Add a 'program' early-exit to
buildEnclosingQualifiedName (ruby/captures.ts). Behavior-preserving.

U8 (P3): a root-anchored C++ base ": ::A::Inner" names the GLOBAL type, but
resolveQualifiedInheritanceBase prepended the deriving class's enclosing
segments and could mis-bind to an enclosing-relative same-path type. Detect the
leading "::" on the raw qualifier and try only the root-anchored key.
Discriminating fixture + test (registry-primary only).

cpp+ruby+rust resolver suites 599/599; tsc clean. Addresses PR #1981 review
(4417182679) perf + P3 items.

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

* test(ingestion): rebaseline ruby+cpp scope-capture fingerprints for new #1982 fixtures

The four new fixtures (ruby-nested-mixin-shortname, ruby-qualified-mixin,
cpp-namespaced-collision, cpp-global-base-anchor) grow the lang-resolution
corpus, drifting the ruby and cpp order-independent capture fingerprints.
Verified purely additive: the ruby captures golden shows only the two new
fixtures added (existing byte-identical), and removing the two cpp fixtures
reverts the cpp fingerprint to the prior baseline (so the U3/U6/U8 code changes
are scope-resolution / behavior-preserving, not capture-emission). measure.mjs
--check PASS (14 languages).

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

* style(ingestion): prettier-wrap ruby resolver test call (#1982)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:23:17 +01:00
Gergő MagyarandClaude Opus 4.8 f1b8438388 perf(cpp): index ADL candidates once instead of per-site rescans (#1990)
* perf(cpp): index ADL candidates once instead of per-site rescans

C++ scope-resolution `emit` dominated large-repo analysis (~6.76h on a
5,969-file repo — ~70% of the total run). `pickCppAdlCandidates` ran once
per unresolved ADL-eligible call site and each time:

- rescanned every parsed file (rebuilding a per-file scope map per call),
- scanned every workspace def (`findCppClassDefBySimpleName`), and
- used an O(scopes²) child-scope walk for hidden friends.

That is O(unresolved sites × files); with hundreds of thousands of
unresolved C++ sites the emit phase went super-linear. `resolve` (registry
lookup) was only 3.5s — the cost was entirely in fallback edge emission.

Build an `AdlCandidateIndex` once per run (lazy, guarded by `parsedFiles`
identity, reset in `clearCppAdlState`) and query it per site:

- `classDefsBySimple` — preserves `defs.byId` order so first-match /
  ambiguous semantics are identical to the legacy linear scan.
- `nsCandidates` — namespace-owned callables, with inline-namespace
  transparency.
- `friendCandidates` — hidden-friend + class-member callables; a
  parent→children scope index replaces the O(scopes²) walk.
- `nsFunctionsByQName` / `nsFunctionsBySimple` — function-reference ADL path.

A monotonic `seqByNodeId` (file-major; namespace defs before friend/member
defs within a file) lets the per-site query merge candidates across
associated namespaces, dedup by nodeId, and sort — reproducing the exact
legacy candidate set and order.

Per-site cost drops from O(sites × files) to O(associated namespaces); the
emit phase goes from linear-in-sites to flat. Benchmark (files=80): emit at
1000 sites 232ms → 9ms, 2000 sites flat at 17ms; the eliminated term scales
with file count, so the speedup is ~1000×+ on the real 5,969-file repo.

Behavior is unchanged: synthetic candidate output is byte-identical
before/after, all 270 C++ integration resolver tests and 4/4
resolver-parity-expected-failures pass, and tsc + eslint are clean.

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

* docs(cpp): correct ADL state-lifecycle and cache-guard comments

The header lifecycle block listed three module-level maps and named
clearFileLocalNames as the reset caller; both became inaccurate when the
candidate index was added. Enumerate all five state pieces, name the real
caller (loadResolutionConfig), and document that ensureAdlIndex's staleness
guard keys on parsedFiles identity while the index also depends on scopes
and classToNamespaceQualifiedName.

Addresses PR #1990 tri-review (U1, U3). Doc-only; no behavior change.

* test(cpp): guard the ADL seq-coverage invariant in dev/test

pickCppAdlCandidates sorts merged candidates by seqByNodeId with a `?? 0`
fallback. That fallback is unreachable today (every bucketed def is
seq-assigned in the same build block), but a future regression could break
it and silently collapse two seq-0 candidates, dropping a CALLS edge with no
error. Add validateAdlSeqCoverage and run it from buildAdlIndex under the
resolver's opt-in validation gate (NODE_ENV!=production && VALIDATE_SEMANTIC_MODEL!=0),
so a broken invariant throws loudly in dev/CI instead. Production behavior
and the hot path are unchanged. Unit-tested; 270/270 cpp integration tests
pass with the guard active.

Addresses PR #1990 tri-review (U2).

* test(cpp): parity fixture for ADL hidden-friend + namespace-callable merge

pickCppAdlCandidates merges friendCandidates (hidden friends of associated
classes) and nsCandidates (namespace-owned callables) for a single associated
namespace. The byte-identical-parity claim rested only on an uncommitted
harness. Add a fixture that reaches one callable through each bucket — combine
only via a hidden friend, process only via a namespace member — so dropping
either bucket from the merge fails the suite. Candidate order is not observable
(narrowing resolves a unique survivor or suppresses), so the guard is on the set.

Addresses PR #1990 tri-review (U4).

* test(cpp): add ADL emit-scaling benchmark

Guards the PR #1990 optimization against reintroducing the O(sites x files)
ADL candidate scan. Generates many UNRESOLVED ADL sites (class-typed arg +
a callee declared nowhere) and co-scales files and sites with N, so the old
cost is O(N^2) and the new cost O(N). Isolates the scope-resolution emit ms
from parse-dominated wall time via the logger test destination (capture
verified) and asserts the end-to-end emit ratio stays under fileRatio^1.5.
Gated by GITNEXUS_BENCH=1; runs build-free (workerPoolSize: 0).

Addresses the benchmark request alongside PR #1990 (U5).

* test(cpp): add cpp pipeline file-count benchmark

Fills the one missing per-language pipeline benchmark (cobol/csharp/go/php/
ruby/rust already have one); modeled on cobol-pipeline-benchmark.test.ts.
Generates synthetic C++ with constant per-file work and constant header
fan-out, sweeps file count through the full pipeline, and guards linearity
with a coarse time-ratio bound plus a deterministic node-ratio bound (the
non-flaky guard against reintroducing O(fileCount^2) work). Gated by
GITNEXUS_BENCH=1; runs build-free (workerPoolSize: 0).

Addresses the benchmark request alongside PR #1990 (U6).

* style(cpp): prettier-format adl benchmark

* test(cpp): rebaseline scope-capture fingerprint for new ADL fixture

The U4 parity fixture (cpp-adl-ns-plus-hidden-friend-same-name) lives under
test/fixtures/lang-resolution/cpp-*, so its lib.h + app.cpp join the cpp
scope-capture bench corpus (bench/scope-capture/measure.mjs). That is pure
fixture-corpus growth — no scope-extractor change, existing fixtures' captures
byte-identical — so the cpp fingerprint legitimately drifts (fixture_count
265->267). Rebaseline cpp to match, as #1965/#1975 did for earlier fixture
additions. Verified: --check PASS for all 14 languages.

Addresses PR #1990 tri-review (U4 follow-on).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 12:25:42 +01:00
Gergő MagyarandClaude Opus 4.8 fca3494807 fix(embeddings): guard local ONNX runtime on macOS Intel before transformers.js import (#1987)
* fix(embeddings): guard local ONNX runtime on macOS Intel before transformers.js import

macOS Intel (darwin/x64) crashed on `gitnexus analyze --embeddings` with a raw
`Cannot find module .../bin/napi-v6/darwin/x64/onnxruntime_binding.node`: both
embedders imported @huggingface/transformers at module scope, which loads
onnxruntime-node and resolves the (unshipped) native binding before any backend
could be selected. ONNX_WEB_BACKEND=wasm could not help (#1516).

- Add a native-free runtime-support guard (getLocalEmbeddingRuntimeBlocker) that
  returns a clear, actionable message on darwin/x64 and null elsewhere.
- Convert both the core and MCP embedders to type-only transformers imports plus
  a guarded lazy `await import()`; throw the blocker in initEmbedder before any
  transformers.js / onnxruntime-node resolution. HTTP mode is unaffected.
- Surface the blocker cleanly in the analyze CLI instead of the misleading
  "installation may be corrupt" module-not-found hint.
- Add unit tests: guard DI, lazy-import timing, core+MCP darwin/x64 rejection,
  and HTTP mode not blocked.

Refs #1515, #1516

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

* fix(doctor): surface macOS Intel local-embedding limitation

`gitnexus doctor` now reports whether the local embedding runtime can load on
the current platform. macOS Intel (darwin/x64) users see up front that local
embeddings are unavailable — plus the recommended alternatives — instead of
only discovering it when `analyze --embeddings` fails (#1515).

The Embeddings section gains a "Support" line; on a blocked platform the full
guidance (reused from getLocalEmbeddingRuntimeBlocker, single source of truth)
is written to stderr. doctor stays import-safe — it never loads transformers.js
or onnxruntime-node, so it runs cleanly on macOS Intel.

Refs #1515

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

* test(embeddings): close #1515 guard coverage gaps + PR #1987 review polish

Resolves the maintainer tri-review feedback on PR #1987:

- Add the analyze error-branch test (new analyze-local-embedding-error.test.ts):
  a darwin/x64 blocker routes to the clean local-embedding-unsupported message
  (exit 1), not the module-not-found "installation may be corrupt" branch, and
  wins over isHfDownloadFailure even when both match (guards the reorder below).
- Cover the MCP embedQuery darwin/x64 paths — HTTP bypass via httpEmbedQuery
  without importing transformers, and local-mode rejection before the import.
- Make the "defaults platform/arch" guard test falsifiable by stubbing the
  platform, instead of asserting null === null on the CI host.
- analyze.ts: evaluate the blocker-message branch before the network-heuristic
  isHfDownloadFailure branch so the explicit platform message takes priority.
- runtime-support.ts: the blocker message now also notes GITNEXUS_EMBEDDING_DEVICE
  =wasm/cpu cannot help, not only ONNX_WEB_BACKEND=wasm.
- doctor.ts: resolve platform/arch once instead of re-resolving after the guard.

Refs #1515, #1516

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:46:16 +01:00
78ad6bc07e fix(web): align agent system prompt with registered tools (#1984)
* fix(web): align agent system prompt with registered tools

Rewrites BASE_SYSTEM_PROMPT to fix tool-name mismatches, citation format,
and schema guidance from PR #14 tri-review, and adds unit tests that
guard prompt ↔ tool registry parity.

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

* test(web): enforce agent prompt/tools parity and harden assertions

U1: assert GRAPH_RAG_TOOL_NAMES equals the names createGraphRAGTools actually registers (via a no-op stub backend), closing the const<->registration drift gap the prompt-parity test previously missed.

U2: make the forbidden-name guard word-boundary (catches bare-prose mentions, not just backticked); make the highlight_in_graph guarantee registry-level (reword-proof) plus a presence check; add a parser-recognized [[Type:Name]] symbol-citation assertion.

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

* refactor(web): drop test-only GRAPH_RAG_TOOL_NAMES from llm barrel

U3: GRAPH_RAG_TOOL_NAMES has no runtime consumer -- the parity test imports it directly from ./tools -- so remove it from the public index.ts barrel re-export. Update the constant's doc comment to name the registration<->const<->prompt coupling now enforced by agent-prompt.test.ts.

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

* refactor(test): derive symbol-ref assertion from NODE_REF_REGEX

Source the symbol-citation assertion from the UI parser's own NODE_REF_REGEX instead of a hardcoded 4-label subset, so the test tracks the parser's allowlist rather than forking it. Also drop a redundant array spread and an unnecessary readonly-tuple cast surfaced by the simplify pass.

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

* test(web): forbid affirmative highlight_in_graph call instructions

Code review noted the registry-absence + bare-presence pair would pass if a future prompt edit affirmatively instructed calling highlight_in_graph (string present, still not registered). Add an assertion that the prompt never says use/call/invoke highlight_in_graph -- restoring the protective intent of the replaced negation check without its brittleness.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:00:09 +01:00
04ade15451 fix(rust): scope-resolution coverage gaps — F66,F68,F71,F72 (#1934) (#1974)
* fix(rust): scope-resolution coverage gaps — F66,F68,F71,F72,F73 (#1934)

* fix(rust): reviewer fixes — macro namespace, revert pattern:(_), drop variadic

* fix(rust): wire macro resolution end-to-end + materialize unions (#1974 review)

Addresses the outstanding #1974 review (second batch). Per maintainer
decision, F72 is FULLY WIRED rather than documented capture-only.

F72 macro — was a capture-only no-op (@reference.macro dropped downstream):
- gitnexus-shared: add 'macro' ReferenceKind + Reference.kind; add
  MACRO_KINDS (['Macro']) and a MacroRegistry that resolves a macro
  invocation ONLY to a macro_rules! definition — never a same-named free
  function (the disjoint-namespace guarantee the review required).
- scope-extractor: referenceKindFromAnchor @reference.macro -> 'macro';
  normalizeNodeLabel 'macro' -> Macro.
- resolve-references: route 'macro' sites through MacroRegistry.
- emit-references / graph-bridge edges: 'macro' -> USES (kept out of the
  CALLS keyspace, which denotes function/method dispatch).
- node-lookup isLinkableLabel: Macro is linkable, bridging the registry
  def to the legacy @definition.macro graph node.
- rust query: capture macro_rules! as @declaration.macro; fix the scoped
  macro arm to capture the tail identifier, not the full path (P3).

F71 union — the @declaration.struct scope capture had no graph node to
resolve to (legacy RUST_QUERIES never captured union_item):
- legacy query: capture union_item as @definition.struct so the union is
  materialized as a Struct node and is genuinely resolvable.
- query.ts: document the deliberate union->Struct downgrade rationale.

Tests:
- rust.test.ts (parity-gated): pipeline-level union resolution + macro
  resolution (USES to the Macro, exactly one CALLS to fn, none to Macro).
  Macro resolution is registry-primary-only -> listed in
  LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES['rust'].
- rust-coverage.test.ts: scoped-macro tail + macro-def capture assertions;
  reframed as capture-layer only, pointing at the pipeline tests.
- new fixtures rust-macro, rust-union.

F73: dropped from baselines.json _note (variadic was never implemented).

Rebaselined the rust capture golden + scope-capture fingerprint
(a5fdff2c..., scaling ~0.99, fixture_count 126).

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

* style(rust): prettier-format the Reference.kind union (#1974)

CI quality/format gate — collapse the multi-line 'macro' addition back to
one line (fits the 100-col print width).

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 07:24:37 +01:00
evolution 5dcffde9e8 fix(go): generic composite literal constructor inference (F33) (#1976) 2026-06-03 05:24:31 +01:00
Gergő Magyar f01d913eef fix(hooks): resolve gitnexus on PATH with a pure-Node scan, all-OS (#1938) (#1980) 2026-06-03 03:19:49 +01:00
Gergő MagyarandClaude Opus 4.8 5f0d690c60 fix(ingestion): materialize graph nodes for scoped class/module/impl declarations (#1975) (#1977)
* test(ingestion): failing target tests + graph-integrity helper for scoped-declaration nodes (U1, #1975)

Adds findDanglingEdges() and pipeline-level tests asserting that Ruby
namespaced class/module declarations materialize a Class/Trait node with
a resolving HAS_METHOD edge. Red by design on the pre-fix base (5 failing)
— the fix lands in U2 (shared core) + U3 (Ruby enablement).

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

* fix(ingestion): materialize graph nodes for Ruby namespaced class/module declarations (U2/U3, #1975)

Widen the Ruby legacy structure query so `class Foo::Bar` / `module Baz::Qux`
(name field is a scope_resolution node) match @definition.class/.module as
separate top-level patterns. The node is keyed by its full scoped name, which
matches the HAS_METHOD owner id that findEnclosingClassInfo derives from the
same name field — so the previously-dangling ownership edges now resolve, and
distinct namespaces (Foo::Bar vs Baz::Bar) stay distinct nodes (no collision).

No change to findEnclosingClassInfo (zero call-resolution blast radius) and no
scope-extractor/golden/bench impact — the fix is purely the legacy structure
query gate. Finalizes the U1 target assertions to the qualified-name identity.

Validated: 134/134 Ruby resolver tests pass on BOTH legs; tsc --noEmit clean;
dangling HAS_METHOD edges on the ruby-namespaced fixture drop from 3 to 0.

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

* fix(ingestion): resolve C++ out-of-line nested definition method ownership (U4, #1975)

For an out-of-line `struct Outer::Inner { ... }`, the container name is a
qualified_identifier, so findEnclosingClassInfo derived the owner id from the
full `Outer::Inner` text — but the type is keyed by its in-class declaration
(the nested `Inner` node), leaving the method's HAS_METHOD edge dangling.

Reduce a qualified_identifier container name to its tail segment for the owner
id/name, matching how inline nested definitions are already keyed. Node-type
scoped, so Ruby's scope_resolution names stay full (distinct-by-namespace) and
no language is named in shared code. Only out-of-line-def methods (already
dangling) change behavior — zero impact on bare classes or call resolution.

Validated: C++ 268/268 default leg, 205+63-skip legacy leg, no regression;
2 new target tests pass both legs; Ruby namespaced tests still pass; tsc clean;
scope-capture bench rebaselined (cpp +cpp-out-of-line-class fixture) — --check
PASS (13 langs). Dangling HAS_METHOD on the new fixture: 1 -> 0.

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

* fix(ingestion): resolve Rust scoped impl-target method ownership (U5, #1975)

`impl path::Type` and `impl Trait for path::Type` name the target with a
scoped_type_identifier. Two coordinated fixes:
- findEnclosingClassInfo: reduce a scoped_type_identifier impl target to its
  trailing type name (both the trait-impl `for` branch and the inherent
  branch), matching the type's own tail-keyed declaration.
- tree-sitter-queries: add a @definition.impl arm for scoped inherent impls so
  the Impl node is materialized (keyed by the same tail) instead of missing.

Together the trait-impl method owns through the real Struct node and the
inherent-impl method owns through a real Impl node — no dangling edges. Rust's
scoped_type_identifier has a name: field, so the tail extraction is exact.

Validated: Rust 163/163 on BOTH legs, no regression; new target test passes;
C++/Ruby suites unaffected; tsc clean; scope-capture bench rebaselined
(rust +rust-scoped-impl fixture) — --check PASS (13 langs). Dangling 1 -> 0.

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

* test(ingestion): cross-namespace collision test + regenerate ruby/rust captures goldens (U6, #1975)

- Add ruby-tail-collision fixture + test: Foo::Bar and Baz::Bar share the tail
  'Bar' but must stay two distinct Class nodes (locks the KTD-2 anti-collision
  guarantee from full-scoped-name keying). No dangling, no cross-wiring.
- Regenerate the ruby + rust captures goldens for the fixtures added in U3-U6
  (ruby-tail-collision, rust-scoped-impl). Both diffs are additive-only — a
  single new entry each, existing entries byte-identical (no capture-logic
  drift; the fixes are in the legacy structure query + findEnclosingClassInfo,
  not the scope-extractor).
- Re-baseline the ruby scope-capture fingerprint (81->82 fixtures).

N/A-language verification: C#/Java/PHP have no class-declaration scoped-name
gap and show no regression (606 passed; the 2 C# worker-pool failures are the
known worktree 'parse-worker.js not built' limitation, unrelated to this change).

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

* revert(ingestion): drop C++/Rust scoped-owner reduction; ship Ruby-only (#1975)

The self-tri-review of PR #1977 (review 4411683756) found — and reproduced —
that the C++/Rust tail-reduction in findEnclosingClassInfo collides same-tail
types declared in the same file (struct Outer::Inner + struct Other::Inner ->
one Struct:Inner node, methods silently mis-attributed; same-named members
merge). Root cause is pre-existing: GitNexus keys nested-type nodes by their
tail name within a file, so even plain inline same-tail nested types already
merge. A correct fix needs fully-qualified nested-type node identity — a broad
change deferred to #1978.

This reverts the C++ (qualified_identifier) and Rust (scoped_type_identifier
impl) owner reductions in ast-helpers.ts, the Rust @definition.impl scoped arm,
and the cpp/rust fixtures+tests+golden+bench entries. The Ruby fix is unaffected
(it keys the node by the full scoped text — no collision) and stays:
namespaced class/module node materialization + the cross-namespace collision test.

Validated Ruby-only: 136/136 both legs; ruby+rust captures goldens 19/19;
bench --check PASS (14 langs); tsc clean.

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

* fix(ingestion): collision-safe C++/Rust scoped-declaration node ownership (#1975)

Re-introduces the C++/Rust fix the tri-review reverted, using a collision-safe
approach instead of owner tail-reduction (which merged same-tail types in one
file). Key the scoped DECLARATION's node by its full qualified text so it
matches the owner id and stays distinct from a same-tail type elsewhere:

- C++: widen the legacy structure query to materialize a node for out-of-line
  defs (class/struct Outer::Inner — name is qualified_identifier), keyed by the
  full text. No findEnclosingClassInfo change needed — BASE already derives the
  full-text owner, which now matches. Outer::Inner and Other::Inner stay
  distinct; 3-level A::B::C resolves. (A redundant forward-decl node remains.)
- Rust: @definition.impl arm for scoped inherent impls (keyed full) +
  findEnclosingClassInfo inherent-impl branch accepts scoped_type_identifier
  with full text. impl a::Inner and impl b::Inner stay distinct.

Collision-aware fixtures + positive owner-identity assertions (per the
tri-review) replace the single-type fixtures. Deferred to #1978: Rust trait
impls on a scoped struct path (impl T for a::Inner) and the pre-existing inline
same-tail node collision — both need qualified struct-node identity.

Validated: Ruby 136/136, C++/Rust 434/434 both legs (371+63-skip legacy);
ruby+rust captures goldens 19/19 (additive); bench --check PASS (14 langs);
tsc clean.

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

* chore(format): apply prettier to scoped-declaration changes (#1975)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:47:15 +01:00
Gergő MagyarandClaude Opus 4.8 de0248c5db refactor(ingestion): migrate Dart to registry-primary call resolution (#939) (#1970)
* feat(scope-resolution): migrate Dart to registry-primary call resolution (#939)

Add a Dart scope-resolution module (languages/dart/) mirroring the Swift
template and flip Dart to registry-primary. Resolution edges
(CALLS/IMPORTS/ACCESSES/EXTENDS/IMPLEMENTS/METHOD_IMPLEMENTS) now route
through the shared registry pipeline with byte-for-byte parity against the
legacy DAG: test/integration/resolvers/dart.test.ts passes 53/53 under both
REGISTRY_PRIMARY_DART=0 and =1 (scripts/run-parity.ts --language dart: 2/2).

Dart-specific handling:
- Function scopes are synthesized to span signature..body (tree-sitter
  function_signature/function_body are siblings, not parent/child).
- extends rides @reference.inherits (EXTENDS via the generic pre-pass);
  implements/with are carried as __heritage__ side-effect imports and
  emitted as IMPLEMENTS, since Dart `implements <class>` must be IMPLEMENTS
  regardless of the target's symbol kind.
- imports are wildcard (whole-library) with expandsWildcardTo so imported
  return types propagate cross-file (var u = getUser(); u.save()).
- getInnerSignature now self-returns a bare signature node so top-level
  function params/return/name extract (legacy-safe: legacy only ever passes
  method_signature/declaration wrappers).

Also: add Dart scope-capture bench coverage (linear ~0.99 scaling); update
two tests that used Dart as a non-migrated control (Vue / forced legacy).

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

* fix(scope-resolution): close Dart registry-primary parity gaps from review

Adversarial review of #1970 surfaced real divergences from the legacy DAG on
constructs the 10 fixtures don't exercise. All fixed; parity gate still 2/2
(now 55/55 each mode):

- Implicit-constructor construction (`Foo()` with no explicit ctor): the
  legacy DAG emits `caller -> Foo` (Class) but registry emitted nothing
  (callee tagged @reference.call.free never reaches constructorCallTargetsClass).
  Re-tag UpperCamelCase free-callees to @reference.call.constructor (Dart types
  are UpperCamelCase) so they link to the Class. Locked in with a regression
  fixture + test that passes in BOTH modes.
- Cascade calls (`list..add(1)..sort()`) were dropped — cascade_section has no
  `selector` wrapper, so the reference walk never saw them while legacy emitted
  them as free calls. Add a cascade_section handler.
- BUILT_INS (setState/then/push/pop/listen/...) were not suppressed on the
  registry path, so a user symbol shadowing one produced a spurious CALLS edge
  the legacy DAG suppresses. Skip built-in-named call refs at capture time
  (extract the set to a leaf module shared with the provider).
- Enhanced-enum methods mis-parented to Module (no enum scope). Add
  `(enum_declaration) @scope.class` so enum members are owned by the enum.

Re-baseline the Dart scope-capture fingerprint (linear ~0.95).

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

* feat(scope-resolution): apply issue #1926 F24/F25 findings to the Dart scope path

Issue #1926 catalogs Dart parsing-layer coverage gaps. Apply the two that the
registry-primary scope-resolution path owns (call edges + call attribution),
registered as legacy-expected-failures since they are scope-resolver-only wins.

- F24: the scope path's unified tree-walk already captures member calls
  (obj.method()) in return / list-literal / named-argument / arrow-body
  contexts — the legacy DAG only captures them under expression_statement /
  initialized_variable_definition. Lock it with the dart-member-call-contexts
  fixture + tests.
- F25 (constructor portion): a constructor's body is a sibling of the WRAPPING
  method_signature (class_body > method_signature > constructor_signature, then
  function_body), so findFunctionBody now walks up to the method_signature
  wrapper. Constructor bodies get a Function scope and their body-calls
  attribute to the Constructor (a valid caller anchor) instead of the class.
  Add the dart-constructor-body fixture + test.

Switch dart.test.ts to createResolverParityIt('dart') and add the dart entry to
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES (5 wins). Both modes pass:
run-parity --language dart → 2/2 (registry 60/60; legacy 55 pass + 5 skipped).

Not applicable to the scope path (structure-phase / shared-pipeline, tracked by
#1926's legacy fix): F25 getter/setter (Property is not a caller anchor) and
operator (no Method node emitted by the structure phase) bodies; F26 (static
field Property nodes); F27 (no generic_type reference in the scope module);
F28/F29 (typedef/variable node extraction). Re-baseline the Dart scope-capture
fingerprint (linear ~1.0).

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

* fix(scope-resolution): fix Dart named-constructor file-drop + container-name mis-binding (tri-review)

Multi-engine tri-review (GitNexus + CE personas + Codex gpt-5.5) of #1970
found a P0 the parity gate missed plus a P2 wrong-edge:

- P0 (file drop): a named constructor with a body (`class A { A.named() {…} }`,
  idiomatic Dart) parses as ONE constructor_signature carrying multiple `name:`
  fields, so the scope query matched it more than once and synthesized two
  identical-range @scope.function captures → ScopeTreeInvariantError(duplicate-
  scope-id) → extractParsedFile swallowed it → the WHOLE file was dropped from
  registry-primary resolution (CALLS=0 vs legacy CALLS=2). Introduced by the
  #1926 F25 findFunctionBody change that started giving constructors body
  scopes. Fix: dedup function-like declarations by their statement node so each
  is emitted once. Add dart-named-constructor-body fixture + a parity guard test
  (both modes) that fails if the file is dropped, plus the named-ctor F25
  attribution win (registry-only).

- P2 (wrong edge): normalizeDartType's Future<X>/List<X> unwrap is unreachable
  (generic args are stripped upstream to a bare `Future`/`List`), so a return/
  field type binding to the bare container name let a same-named user class
  (`class Stream {…}`) capture the receiver — a wrong CALLS edge legacy didn't
  emit. Suppress type bindings that normalize to a bare container name (leaving
  the call unresolved, matching legacy) instead of binding to the container.

Both modes still pass: run-parity --language dart → 2/2 (registry 62/62; legacy
56 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. Also: refresh
the captures.ts module doc (constructors get scopes; cascade calls).

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

* fix(scope-resolution): address Dart tri-review follow-ups (heritage collision + polish)

- P2 heritage cross-file name collision: emitDartHeritageEdges resolved both
  child and base by a global last-write-wins simple-name map, so two files each
  declaring `class Logger` (one `implements Logger`) produced a wrong-file
  IMPLEMENTS edge. Resolve with same-file affinity (prefer a same-file class,
  then a workspace-unique match, else refuse to guess) — the #1951 file-affinity
  pattern. Add dart-heritage-name-collision fixture + a parity test (both modes
  resolve same-file). Also reason-qualify the dedup key so `implements X` + `with X`
  keep distinct edges.
- Polish: buildDartMro uses Sets instead of Array.includes-in-loop; merge-bindings
  uses named tier constants matching swift; drop the dead no-op stripQuotes in
  import-target (targetRaw already arrives quote-stripped).

Both modes pass: run-parity --language dart → 2/2 (registry 63/63; legacy 57 + 6
skipped). Re-baseline the Dart scope-capture fingerprint.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 15:54:00 +01:00
SparshandGergő Magyar 6643afbcda fix(ruby): scope-resolution namespaced class/module definitions — F62 (#1933) (#1972)
* fix(ruby): namespaced class/module definition captures — F62 (#1933)

* chore(bench): regenerate Ruby golden captures after F62 scope_resolution patterns

* fix(ruby): namespaced class/module definition captures — F62 (#1933)

* chore: remove unused imports from ruby-namespaced test

* chore: add comment about capture-only scope in ruby-namespaced test

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-02 13:20:01 +01:00
azizur100389 0a612a31c6 fix(cpp): capture uninitialized multi-declarators (#1965) 2026-06-02 11:38:41 +01:00
evolution 052319324d feat(go): infer structural interface implementations (#1966) 2026-06-02 09:27:44 +01:00
f885330b34 fix(cli): steer docs, skills, and hooks through a CLI-neutral project-local runner (#1939) (#1945)
* fix(cli): steer npm 11 users away from npx install crash (#1939)

Prefer global gitnexus or pnpm dlx in hooks and generated AI context, warn
when npm 11.x would use the broken npx path, and document workarounds for
the arborist node.target null failure mode.

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

* test(hooks): stage resolve-analyze-cmd.cjs for antigravity adapter; harden load checks

The antigravity adapter gained a top-level require('./resolve-analyze-cmd.cjs')
but stageAdapter() did not copy it, so the spawned adapter crashed with
MODULE_NOT_FOUND. Three load-sensitive tests failed; four silent-path tests
false-passed on empty stdout.

Stage the helper alongside the other sibling helpers, and assert status===0 and
no MODULE_NOT_FOUND on the four silent-path tests so a non-loading hook can never
pass green again. Force a deterministic invocation mode in the stale-index test
so the emitted analyze command no longer varies by CI-runner PATH.

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

* fix(cli): standardize invocation hints on gitnexus@latest; single-source CJS helper

NPX_REF becomes a literal `gitnexus@latest` in resolve-invocation.ts, dropping
the package.json require and the module-load throw (a malformed/absent version
can no longer crash any CLI command at import). The safety this PR delivers is
the install method steered to (global / pnpm dlx), not a pinned gitnexus
version, and the in-repo CJS mirror already degraded to `latest` once copied
outside the package.

Make the two resolve-analyze-cmd.cjs copies byte-identical and add a parity
test that fails on drift. The separate, version-pinned NPX_REF that setup.ts
writes into the MCP server registration is intentional and left unchanged.

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

* perf(cli): move npm-11 npx warning off module load; memoize invocation mode

warnIfNpm11NpxRisk() ran at index.ts module load, so every CLI invocation
(including the `gitnexus mcp` stdio hot path) paid which/where + npm --version
spawns — against the lazy-startup/MCP-stdout discipline (#207, #1383). Move the
call into analyzeCommand, after the ensureHeap() re-exec guard, so it fires once
in the working process and only for `analyze`.

Memoize the PATH-probe-derived invocation mode (the GITNEXUS_INVOCATION override
stays uncached) so repeated callers don't re-probe, and add a test-only reset so
the cache + once-only warning flag don't leak across the unit suite. Covers the
mode!=='npx', npm<11, and npm-absent suppression branches.

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

* fix(cli): detect .exe/extensionless global gitnexus shims on Windows

The winGitnexusWrapper branch only matched .cmd/.bat, so a global gitnexus
installed by Volta or scoop (a .exe or an extensionless shim) was missed and the
hint fell back to pnpm/npx. Accept .exe and treat any non-empty `where` hit as
on-PATH (the emitted hint is `gitnexus analyze` regardless of which shim
resolves it). Mirror the change into both resolve-analyze-cmd.cjs copies so the
TS source and the byte-identical hook mirrors stay in sync.

Add Windows-mocked test cases (.exe-only, extensionless, .cmd preference, CRLF
stripping) and register resolve-invocation.test.ts in cross-platform-tests.ts so
the windows-latest runner exercises the branch.

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

* fix(cli): emit fixed pnpm dlx analyze command in generated AGENTS.md/CLAUDE.md

ai-context baked a machine-resolved command (formatAnalyzeCommand) into
git-tracked AGENTS.md/CLAUDE.md, so the stale-index hint varied per machine and
churned across branches (the #1706 class). Emit the fixed string
`pnpm dlx gitnexus@latest analyze` instead: committed AI-context is the most
authoritative instruction an agent reads, so it must name an install-free,
crash-free method — never `npx`, the npm-11 path #1939 steers away from.

formatAnalyzeCommand stays exported and unit-tested in resolve-invocation.ts
(it still mirrors the two .cjs hook copies); ai-context just no longer calls it.

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

* refactor(cli): unify hook-helper copy into one non-silent routine

installClaudeCodeHooks copied its four hook helpers in separate try/catch blocks
that silently swallowed failures, while installAntigravityHooks recorded an
error per failed copy. Extract one copyHookHelpers(srcDir, destDir, label,
result) with a single canonical helper list (including resolve-analyze-cmd.cjs)
and the antigravity loop's error-reporting policy, and use it from both paths so
a missing helper surfaces as a setup error instead of a silent runtime crash.

Assert both the Claude and Antigravity install paths co-locate
resolve-analyze-cmd.cjs next to the adapter, and that a failed copy records an
error rather than passing silently.

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

* docs(cli): reattach installClaudeCodeHooks JSDoc after helper extraction

The extracted HOOK_HELPERS/copyHookHelpers block landed between the
installClaudeCodeHooks JSDoc and its function, leaving the doc reading as if it
described the helper list. Move the block above the doc so it documents the
function again. No behavior change.

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

* test(cli): enforce TS<->CJS invocation parity and guard CLI startup posture

Tier-2 review found two in-scope gaps in the #1945 follow-up:

- The "mirrors resolve-invocation.ts / test enforces parity" comments overclaimed:
  the parity test only compared the two .cjs copies to each other, so the TS
  source and the CJS hook copies could silently drift (NPX_REF, the per-mode
  command, and the Windows shim regex were hand-edited in all three this PR).
  Add TS<->CJS value parity (NPX_REF + formatAnalyzeCommand for every forced
  mode) and a source-level shim-regex parity check, and make the mirror comments
  accurately describe what is enforced.

- No test locked the R3/R4 startup posture, so re-adding warnIfNpm11NpxRisk()
  (or any resolve-invocation import) at index.ts module scope -- the #207/#1383
  lazy-startup regression -- would pass CI. Add a guard asserting index.ts has
  no module-load invocation probe and the warning is wired into analyzeCommand.

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

* refactor(cli): collapse npx-invocation resolver to one source of truth

PR #1945 carried the gitnexus/pnpm/npx selection in three hand-synced
places — the canonical hook helper, its byte-identical plugin copy, and a
full TypeScript re-implementation in resolve-invocation.ts — kept in lockstep
by per-mode-command and regex-extracted-by-regex parity tests. The TS
formatAnalyzeCommand had no production caller (ai-context emits a fixed
string), and the module memoized + exposed a test-only reset for a "repeated
callers" case that has exactly one caller.

Make hooks/claude/resolve-analyze-cmd.cjs the single source: extract the
Windows-shim line-picking into a pure, exported pickPathMatch() and add an
injectable probe to resolveInvocationMode() so the shipped logic is testable
without spawning or global mocks. resolve-invocation.ts (118 -> 59 lines) now
consumes that cjs via createRequire for resolveInvocationMode/NPX_REF and adds
only the CLI-only npm-version probe and warning; the relative path resolves
identically from src/cli/ (tsx, vitest) and dist/cli/ (shipped, hooks/ is a
published sibling of dist/). Tests exercise the real shipped artifact, the
NPX_REF/mode-command parity scaffolding is dropped (one implementation can't
drift), and parity narrows to the two cjs copies staying byte-identical.

No behavior change: hook stale-index hints and the analyze warning are
byte-identical; the pre-existing setup.ts resolveGitnexusBin is untouched.

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

* fix(cli): bound stale-index hook PATH probe under the hook budget (U1)

The PostToolUse stale-index hint calls formatAnalyzeCommand(), which probes which/where; named PROBE_TIMEOUT_MS=2000 keeps git rev-parse (~3s) + up to two probes well under Claude Code's 10s hook timeout while preserving the machine-correct hint. Byte-identical in the plugin copy.

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

* fix(cli): steer generated cross-repo group commands off npx (#1939) (U2)

The Cross-Repo Groups block in generated AGENTS.md/CLAUDE.md still emitted bare 'npx gitnexus group ...', funneling npm-11 users into the arborist crash; switch to fixed 'pnpm dlx gitnexus@latest group ...'. Export generateGitNexusContent and add a group-branch test asserting no 'npx gitnexus' literal survives.

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

* docs: align steering guidance on pnpm dlx gitnexus@latest (U3)

README troubleshooting uses gitnexus@latest; the repo's own committed CLAUDE.md/AGENTS.md stale-index hint now matches the generated output (pnpm dlx gitnexus@latest analyze) so the repo dogfoods the fix.

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

* test(hooks): assert exact @latest analyze command and pin invocation mode (U4)

Drop dead PKG_VERSION/NPX_REF version-pinned constants; the cjs always emits gitnexus@latest, so assert exact toContain(...) instead of the /@\\S+/ wildcard; pin GITNEXUS_INVOCATION in the --embeddings tests for host-independent determinism.

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

* test(cli): cover resolver warn/edge branches; document probe seam (U5)

Add coverage for the gitnexus-mode warn suppression, getNpmMajorVersion edge inputs (empty/pre-release/non-numeric), and the Windows non-wrapper pickPathMatch branch; widen the InvocationResolver interface to document the optional probe param.

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

* fix(cli): lower hook PATH-probe timeout to 1000ms (U1)

In a linked worktree the stale-index hook runs git rev-parse --git-common-dir (~2s) + rev-parse HEAD (~3s) before up to two PATH probes; PROBE_TIMEOUT_MS=1000 holds the worst case near ~7s under Claude Code's 10s hook budget (was 2000, ~1s headroom). Byte-identical in the plugin copy.

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

* fix(cli): fail closed in gitnexus setup on missing required hook helper/adapter (U2)

copyHookHelpers now returns the failed REQUIRED helpers (the .cjs trio; win-rm-list-json.ps1 stays best-effort since it fails open). Both install paths skip hook registration with an actionable error when a required helper failed; the Claude path also gains the adapter-existence guard the Antigravity path already had. Prevents registering a hook that crashes MODULE_NOT_FOUND on every tool event.

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

* fix(skills): steer committed skill files off npx to pnpm dlx gitnexus@latest (U3)

All 26 committed skill-file copies (gitnexus/skills, .claude, plugin, cursor) used 'npx gitnexus analyze', contradicting the generated freshness line and funneling npm-11 users into the arborist crash. Replace with 'pnpm dlx gitnexus@latest analyze'; add a regression guard (skills-steering.test.ts) that globs all four locations and fails if any reintroduces it. The cli skill's non-analyze npx subcommands (status/clean/list/wiki) are left as-is (out of the analyze-funnel scope).

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

* fix(cli): guard resolver import shape; assert group-impact steering (U4)

Add a load-time guard on the createRequire(resolve-analyze-cmd.cjs) cast so a drifted/renamed cjs export fails loudly at module load instead of as a late TypeError in warnIfNpm11NpxRisk. Add the missing 'group impact' assertion to the ai-context Cross-Repo Groups test, and a resolver-contract test.

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

* fix(cli): auto-select invocation path with pnpm --allow-build (#1939)

Probe npm/pnpm versions and PATH to pick a working analyze command without
user configuration: global gitnexus first, pnpm dlx with --allow-build on
npm 11+ (Ladybug native scripts), npx on npm 10 and earlier. Update docs,
skills, and tests to match the canonical install-free command.

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

* fix(cli): place pnpm --allow-build before dlx, repair version-injection seam (#1939)

The auto-selected install command emitted `pnpm dlx --allow-build=… analyze`,
but pnpm < 10.14 keeps `dlx` in its argv escape list, so flags placed *after*
`dlx` are parsed as package specs and rejected (ERR_PNPM_SPEC_NOT_SUPPORTED) on
pnpm 10.2–10.13.x — strictly worse than the bare command. Move the flags before
`dlx` (the position pnpm has honored since 10.2.0) in both byte-identical hook
copies, the committed AGENTS.md / CLAUDE.md, and every skill tree.

Also repairs the CI-red resolveInvocationMode seam: injecting `{ npmMajor: null }`
to simulate an absent npm fell through `??` to the host's real `npm --version`
(npm 10.x on the CI runners → routed 'npx' instead of 'pnpm'). Use an
`'npmMajor' in deps` sentinel so an injected null is honored, drop the dead
parseMajorVersion guard, and gate the flags on pnpm >= 10.2 via a single
minor-aware probeVersion spawn (skipped for committed docs). Align the TS
getNpmMajorVersion timeout to the 1s hook budget and strengthen the
skills-steering guard with a pre-dlx positive assertion plus a post-dlx
regression check.

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

* docs: add npm-11 pnpm caveat to README Quick Starts (#1939)

The root, package, and cursor-integration README Quick Starts still steered
first-contact users to bare `npx gitnexus analyze` — the exact npm 11.x
arborist install crash issue #1939 names as a funnel. Add a one-line pnpm
`--allow-build … dlx` caveat (keeping the simple npx default for npm<=10 /
pnpm / yarn users); the package README points to its existing npm-11
workaround section.

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

* docs(skills): route every gitnexus-cli command off npx to pnpm dlx (#1939)

The gitnexus-cli skill demonstrated analyze via `pnpm --allow-build … dlx`
but still showed status/clean/wiki/list via bare `npx gitnexus` — the same
package, the same npm-11 crash-prone install path — and its header claimed
"all commands work via npx". Convert every subcommand to the pnpm form across
all three skill copies and reconcile the header. Broaden the skills-steering
guard to forbid any `npx gitnexus` command in the cli-skill copies.

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

* perf(hook): probe pnpm once on the stale-index path (#1939)

The stale-index hook resolved pnpm twice — `which pnpm` for mode selection
then `pnpm --version` for the allow-build gate — two spawns for one tool in a
~9s/10s budget. Capture the version once in formatAnalyzeCommand and thread it
through the existing deps seam (a successful `pnpm --version` proves presence),
sharing a memoized PATH probe with resolveInvocationMode. Add explicit pnpm
10.0-suppress / 10.2-emit boundary tests and relabel the unknown-minor case.
Both byte-identical cjs copies updated together.

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

* fix(setup): single-quote POSIX hook command + assert cliPath patch applied (#1939)

The hook `command` written into editor settings is shell-evaluated; the
double-quoted `node "<path>"` form left `$`, backtick, and other metacharacters
live in an adversarial $HOME. Single-quote the path on POSIX (Windows keeps the
double-quoted form — those chars are illegal in Windows filenames). Also assert
the cliPath source-literal replace() actually matched, recording an actionable
error on drift instead of silently shipping a hook with an unresolved relative
path.

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

* test(setup): normalize expected hook path for the Windows runner (#1939)

The new POSIX-escaping test built its expected hook path with path.join,
which emits backslashes on the Windows runner, while setup.ts forward-slash-
normalizes the path before quoting — so `expect(cmd).toBe(node '<path>')`
mismatched on tests/windows-latest. Normalize the expected path the same way.
Production code was already correct; only the test's expected value was
platform-fragile.

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

* fix(cli): steer docs/skills via a project-local runner, not a pnpm default (#1939)

The prior approach hardcoded `pnpm --allow-build=… dlx gitnexus@latest <cmd>`
into every committed skill + the generated AGENTS.md/CLAUDE.md, which assumes
pnpm is installed. Replace it with a CLI-neutral project-local runner:

- `gitnexus analyze` drops `.gitnexus/run.cjs` (a copy of the canonical
  `resolve-analyze-cmd.cjs`, which gains `buildRunnerArgv` + a `require.main`
  exec tail) next to the index. Docs/skills reference `node .gitnexus/run.cjs
  <cmd>`, which auto-selects the runner (global `gitnexus` → `pnpm dlx` → `npx`)
  at call time — no package-manager assumption. README first-run + an inline
  bootstrap note stay universal `npx gitnexus analyze`.
- The exec tail uses `shell` on Windows so `.cmd`/`.ps1`/`.exe` shims resolve
  (execFileSync can't otherwise; Node blocks `.cmd` without a shell,
  CVE-2024-27980), and prints a diagnostic instead of a silent exit 1.

Tests: runner exec-tail (real spawn, exit-code propagation + ENOENT diagnostic),
copy-failure graceful degradation, and per-subcommand routing + pnpm-fallback
vacuity guards. The generated CLAUDE.md block stays under the #856 token budget.

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

* fix(cli): resolve Windows .cmd version probes so pnpm steering fires (#1939)

probeVersion (and the TS getNpmMajorVersion mirror) spawned npm/pnpm
--version via execFileSync with no shell, so on Windows the .cmd shims
ENOENT'd, the probe reported a present tool as absent, and the stale-index
hook recommended the npx crash path #1939 exists to avoid. Add
shell: process.platform === 'win32' to the version probes (the exec tail
already does this). Parse the first version-shaped line so a Corepack/notice
banner on stdout no longer defeats the parse. Carry pnpm presence separately
from version so a present-but-unparseable pnpm still selects pnpm. Drop the
dead probe ?? resolveOnPath coalesce. Cover resolve-analyze-cmd.cjs (+ plugin
twin) with the shell-injection and windowsHide source-regression guards.

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

* fix(cli): widen pnpm allow-build for the --embeddings=N equals form (#1945)

buildRunnerArgv detected embeddings via gitnexusArgs.includes('--embeddings'),
which missed the equals form (--embeddings=5000) that Commander also accepts,
dropping --allow-build=onnxruntime-node on pnpm 10.2+. Match both forms.

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

* test(cli): cover the runner exec-tail Windows shell branch on CI (#1945)

runner-exec-tail.test.ts was POSIX-only and unregistered in
cross-platform-tests.ts, so the run.cjs Windows shell:true exec branch ran on
no platform despite the file comment claiming windows-latest covered it. Add a
.cmd-shim it.skipIf(onPosix) case and register the file in SPAWN_CLI so the
windows-latest job runs it.

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

* docs: fix broken troubleshooting anchor in gitnexus README (#1945)

The npm-11 quick-start note linked to #npx-gitnexus-crashes-with-nodetarget-is-null-npm-11,
which matches no heading; the actual troubleshooting heading slugifies to
#cannot-destructure-property-package-of-nodetarget-as-it-is-null. Repoint the link.

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

* test(hooks): guard resolve-analyze-cmd.cjs in antigravity e2e sanity check (#1945)

The antigravity adapter top-level require()s resolve-analyze-cmd.cjs, but the
beforeAll helper-presence loop did not check for it — a failed copy would
surface as noisy MODULE_NOT_FOUND in downstream tests instead of the intended
actionable 'Helper not installed' error. Add it to the loop.

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

* docs(skills): tie a missing-runner Cannot-find-module error to recovery (#1945)

Generated CLAUDE.md/AGENTS.md make `node .gitnexus/run.cjs` the primary
command, but the runner is gitignored, so a fresh clone or git clean leaves an
agent facing a raw MODULE_NOT_FOUND. The CLAUDE.md block is token-budget-capped
(#856), so the recovery guidance lives in the cli skill (its documented home):
the bootstrap note now names the `Cannot find module` error and points at
`npx gitnexus analyze` to (re)generate the runner.

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

* refactor(cli): disambiguate the MCP-pinned ref from the @latest hint (#1945)

setup.ts and resolve-analyze-cmd.cjs both exported a constant named NPX_REF
with different values (version-pinned for the persisted MCP entry vs.
gitnexus@latest for hints). Rename setup.ts's module-private constant to
MCP_PINNED_REF (value and behavior unchanged — the MCP pin stays pinned),
leaving the cjs hint ref and its re-export alone. Also route the createRequire
cast through 'unknown' so it reads as an explicit narrowing to the subset this
module uses rather than a claim about the cjs's full export shape.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:00:34 +01:00
SparshandGergő Magyar fcddbb0818 fix(python): scope-resolution coverage gaps — F57, F58, F61 (#1932) (#1964)
* fix(python): scope-resolution coverage gaps — F57, F58, F61 (#1932)

F57: heritage patterns for qualified/subscripted bases
F58: decorator patterns for nested-attribute decorators
F61: lambda captured as @scope.function
F59 already closed by #1920, F60 legacy-only

* chore(bench): update Python scope-capture baseline after F57/F58/F61

* chore: lower coverage thresholds after F57/F58/F61 query additions

* P0-P6 review fixes: F58 decorator wiring, deduplication, e2e test, golden regeneration, thresholds reverted, baseline update

* chore: remove unused imports from python-parsing-coverage test

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-02 08:12:32 +01:00
Gergő MagyarandSparsh 7691abf76a fix: JS/TS scope-resolution coverage gaps — F44, F83, F85, F86, F87 (#1929) (#1968)
* fix: JS/TS scope-resolution coverage gaps — F44, F83, F85, F86, F87 (#1929)

F44: Add (class) @scope.class for class expressions in TS query.
F83: Fix qualified new_expression (new ns.Foo()) to capture @reference.name.
F85: Add enum member declaration patterns (bare + valued) as @declaration.property.
F86: Unblocked by F44 — class expression methods get correct Class scope.
F87: Add 4 missing optional_parameter type annotation patterns (predefined_type,
     union_type, array_type, readonly_type) matching required_parameter.

Grammar verification via node-types.json confirms all node types exist.
9 new tests proving each fix fails on main and passes on the branch.

* chore(bench): update TypeScript scope-capture baseline after F44/F85/F87

---------

Co-authored-by: Sparsh <sparshprajapati2002@gmail.com>
2026-06-02 06:57:28 +01:00
bfe8a87831 fix: actionable error + docs for pnpm dlx / pnpx native-load crash (#307) (#1967)
* fix: guide pnpm dlx/pnpx users through skipped native install

`pnpm dlx gitnexus serve` (and `pnpx gitnexus`) crash with a raw
`ERR_DLOPEN_FAILED` stack trace because @ladybugdb/core's native addon
(lbugjs.node) is placed by a postinstall script, and dlx/pnpx run
ephemerally without executing lifecycle scripts.

The existing checkLbugNative() guard already catches the missing binary
for serve/mcp/analyze, but its guidance only mentioned bun and
--ignore-scripts. Extend the message to call out the common pnpm dlx /
pnpx case and the fix (`pnpm add -g gitnexus && pnpm approve-builds -g`,
or use npx/npm). Add a matching README troubleshooting section.

This does not make `pnpm dlx` itself work — that requires a runtime
fallback in @ladybugdb/core. It turns the crash into actionable guidance.

Refs #307

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

* fix: add pnpm --allow-build dlx option to native-check guidance

Incorporates collaborator feedback (magyargergo): pnpm's security model
allows `dlx` to run build scripts when you pass `--allow-build` for each
native dep. Add this as the first/preferred pnpm-dlx path in the error
message, README troubleshooting section, and test assertion. Drop the
now-incorrect claim that `pnpm dlx` "cannot be made to work directly".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address PR review on pnpm dlx native-load guidance

Replace removed pnpm approve-builds -g with add -g --allow-build flags,
qualify npm 11 npx caveats, use serve in examples, extend load-failure hints,
and assert --allow-build precedes dlx in tests.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:54:25 +01:00
Gergő MagyarandCursor 7ab6bd36d4 ci(devcontainer): harden smoke build against Docker Hub flakes (#1969)
* ci(devcontainer): retry Docker Hub syntax frontend and build

The devcontainers CLI injects `# syntax=docker/dockerfile:1`, which BuildKit
fetches from Docker Hub. Transient Hub timeouts caused main smoke failures
(run 26797815133). Pre-pull the frontend with backoff and retry the build
once, matching docker-build-push-retry policy.

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

* ci(devcontainer): address tri-review follow-ups on smoke retries

Make syntax-frontend pre-pull best-effort (continue-on-error) so build
retry still runs when Hub flakes only on pull. Clarify comment vs
docker-build-push-retry, and emit a notice when build retry succeeds.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:48:21 +01:00
c4f82e4987 feat(devcontainer): add devcontainer for Claude/Codex/Cursor CLIs (#1875)
* chore: extend .gitattributes for shell scripts and binary assets

Append explicit `*.sh text eol=lf` and `*.bash text eol=lf` rules so
shell scripts (notably anything COPYed into a Linux container) check out
with LF endings on Windows hosts with `core.autocrlf=true`, regardless
of the auto-detection on the existing `* text=auto eol=lf` line. Add
binary markers for `*.node`, `*.wasm`, `*.onnx`, `*.so`, `*.dll`,
`*.dylib` so native and ML model artifacts aren't ever subjected to text
normalization.

The existing `* text=auto eol=lf` and `.husky/* text eol=lf` rules are
preserved. `git ls-files --eol` confirmed zero CRLF or mixed blobs in
the index, so no `--renormalize` was needed.

* feat(devcontainer): add cross-platform devcontainer for Claude Code, Codex, and Cursor CLIs

Add a Dev Container that pre-installs Claude Code (2.1.153, via Anthropic's
official Feature), OpenAI Codex CLI (pinned 0.134.0), and Cursor CLI alongside
the GitNexus native build chain. Opens via VS Code's Dev Containers extension
on Windows 11 (Docker Desktop + WSL2), macOS, or Linux without OS-specific
branches in devcontainer.json.

Topology and base
- Base image `mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm`
  (multi-arch, monthly patched, ships the `node` non-root user, zsh, `gh`).
- Node 22 LTS satisfies `gitnexus/`'s engines `>=22.0.0` and matches the
  `node:22-bookworm-slim` SHA-pinned base used by `Dockerfile.cli`.
- Single container with all three CLIs co-installed (vs. docker-compose
  per-tool) — prevailing 2026 community pattern, lowest daily-driver friction.

Persistence and auth
- Per-devcontainer named volumes scoped by `${devcontainerId}` for
  `/home/node/.claude`, `/home/node/.codex`, `/home/node/.cursor`,
  `/commandhistory`, and `/home/node/.npm`. Authentication survives rebuilds
  without leaking between workspaces.
- Four sub-workspace `node_modules` volumes (root, gitnexus, gitnexus-web,
  gitnexus-shared) keep tree-sitter native bindings and onnxruntime off the
  bind mount — the actual Win/Mac perf win.
- Credential mount paths are pre-created in the Dockerfile with
  `chown node:node` BEFORE `USER node`, so empty named volumes inherit
  correct ownership on first mount and first-run logins don't EACCES.
- `CURSOR_API_KEY` is injected via `containerEnv: ${localEnv:CURSOR_API_KEY}`
  (Cursor's documented headless path); falls back to interactive
  `cursor-agent login` when the host env var is unset.

Build-arg promotion
- Build args (`CLAUDE_CODE_VERSION`, `CODEX_VERSION`, `CURSOR_VERSION`, `TZ`)
  are promoted to ENV in the Dockerfile so lifecycle commands and shells can
  resolve them. Without this promotion, Docker ARG values are build-only and
  silently no-op at lifecycle time.

Workspace setup
- `postCreateCommand` chowns the four workspace `node_modules` volumes
  (Docker creates them root-owned), then installs in dependency order:
  root → gitnexus-shared (install + build) → gitnexus → gitnexus-web. The
  shared package must build before its consumers (`file:../gitnexus-shared`).

Ports
- 5173 (Vite dev) and 4173 (Vite preview) auto-forwarded.
- 4747 (`gitnexus serve`) marked `requireLocalPort: true` because
  `gitnexus-web/src/services/backend-client.ts` hardcodes
  `http://localhost:4747` as the default backend URL; a remapped port would
  silently break the web UI.

VS Code integration
- Recommended extensions: `anthropic.claude-code`,
  `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode`, `eamodio.gitlens`.
- Settings: format-on-save with Prettier, ESLint auto-fix on save, zsh as
  default terminal profile, persistent zsh history via `HISTFILE` →
  `/commandhistory`.

Documentation
- `.devcontainer/README.md` covers WSL2 setup (clone inside WSL2 for IO and
  file-watcher reliability), first-time auth flows for each CLI, port-
  forwarding notes, LadybugDB container limitations, and the bumping
  procedure for each CLI version.
- `CONTRIBUTING.md` gets a "Containerized development (optional)"
  subsection pointing at the devcontainer README.

Deferred to a follow-up PR
- Opt-in egress firewall (originally planned as a fourth implementation
  unit). The Dev Containers spec makes `runArgs` static — toggling
  `NET_ADMIN`/`NET_RAW` capabilities cleanly requires either a separate
  `devcontainer-firewall.json` profile or an `initializeCommand`-generated
  overlay. Keeping this PR focused on the working baseline.
- Codespaces-specific tuning (works incidentally when the firewall is off,
  not actively tested).
- Inside-container Playwright e2e (needs Chromium libs not in the base
  image).

Verification deferred to user
- This change introduces a new dev tooling artifact. Validate by running
  `docker build .devcontainer/`, opening the repo in VS Code via
  "Dev Containers: Reopen in Container", confirming `claude --version`,
  `codex --version`, `cursor-agent --version` resolve inside the container,
  and `cd gitnexus && npm run test:unit` runs clean against the
  named-volume `node_modules`.

* fix(devcontainer): make interactive login the default auth path for all CLIs

The previous `containerEnv` injected `CURSOR_API_KEY: "${localEnv:CURSOR_API_KEY}"`.
When the host had no `CURSOR_API_KEY` set, this resolved to an empty
string and Docker injected `CURSOR_API_KEY=""` into the container.
Cursor CLI treats a set-but-empty `CURSOR_API_KEY` as "use this key"
rather than "fall back to stored login", which silently broke
`cursor-agent login` on the most common path — users who hadn't
explicitly opted into API key auth.

Drop `CURSOR_API_KEY` from `containerEnv`. Login is now the
unconditional default for all three CLIs (Claude Code, Codex CLI,
Cursor CLI); the named-volume + Dockerfile-chown pattern keeps
credentials persistent across container rebuilds for every login path.

Reorganize the README's auth section to put login first for all three
CLIs uniformly (matching the new behavior) and move API key
authentication into a separate "Alternative" section for CI/headless
use. Document that API keys are intentionally not auto-propagated from
the host and explain the export-in-shell or VS Code dotfiles-repo paths
for users who want them. Update the troubleshooting row to reflect the
new design.

* fix(devcontainer): install gitnexus-web before gitnexus in postCreateCommand

The previous order (root → gitnexus-shared → gitnexus → gitnexus-web)
broke at the `gitnexus` install step because `gitnexus`'s `prepare`
script runs `scripts/build.js`, which compiles `gitnexus-web` whenever
its source tree exists. In the devcontainer the entire workspace is
bind-mounted, so `gitnexus-web/` is present from the start — but its
`node_modules/` wasn't yet, so `tsc -b` failed with:

  error TS2688: Cannot find type definition file for 'vite/client'
  error TS2688: Cannot find type definition file for 'node'

Reorder so `gitnexus-web` installs before `gitnexus`. Verified
end-to-end via `npx @devcontainers/cli up`: container builds clean,
all three CLIs (Claude 2.1.153, Codex 0.134.0, Cursor) respond, and
`npx tsc --noEmit` inside `/workspace/gitnexus` passes.

Production Dockerfiles (`Dockerfile.cli` etc.) don't hit this because
they only COPY `gitnexus/` + `gitnexus-shared/`, so `gitnexus-web/`
doesn't exist at install time and `scripts/build.js` skips the web
step. The devcontainer's full-tree bind mount changes that calculus.

* fix(devcontainer): clear stale .husky/_ before npm install

When `npm install` runs the root `prepare` script (husky), husky tries
to copyfile `node_modules/husky/husky` → `.husky/_/h`. On Docker Desktop
Windows bind mounts, if `.husky/_/` already exists from a prior
container run, the new container's `node` user can't overwrite it via
the bind mount's permission translation and the install fails with:

  Error: EPERM: operation not permitted, copyfile
    '/workspace/node_modules/husky/husky' -> '.husky/_/h'

Drop `.husky/_` defensively in `postCreateCommand` before `npm install`
so husky always starts from a clean slate. `.husky/_` is a husky
runtime cache (gitignored), so removing it has no effect on the repo —
husky regenerates it. No-op for WSL2-side checkouts (where this class
of bind-mount permission collision doesn't occur).

Add a troubleshooting row to `.devcontainer/README.md` covering the
manual recovery (`rm -rf .husky/_` on the host) and the long-term fix
(clone in WSL2 — Windows-side bind mounts will keep biting on this
kind of issue across rebuilds with different UID alignment).

* feat(devcontainer): bind-mount host CLI config dirs for plugin/skill/memory sync

Switch the credential/config mounts from per-devcontainer named volumes
to bind mounts of `${localEnv:HOME}/.claude`, `~/.codex`, and
`~/.cursor`. Effect inside the container:

- Authentication is shared with the host. If you've already run
  `claude login` / `codex login --device-auth` / `cursor-agent login`
  on the host, you're already authenticated in the container.
- Plugins, skills, agents, memory, and settings sync both ways. Install
  a plugin in the container, it shows up on the host; add a custom
  agent on the host, the container sees it immediately.
- All devcontainers on the host share the same CLI state, mirroring
  how host shells already share it. (Per-workspace isolation of plugins
  was never a stated requirement; the previous per-devcontainer named
  volumes leaked nothing useful.)

Add `.devcontainer/ensure-host-config-dirs.cjs` and wire it as
`initializeCommand`. It runs on the host before container create and
guarantees `~/.claude`, `~/.codex`, `~/.cursor` exist, so Docker doesn't
reject the bind mount when a CLI has never been used on this host.
Cross-platform via Node `os.homedir()` + `fs.mkdirSync({recursive: true})`;
idempotent; no third-party deps.

Update `.devcontainer/README.md`:
- New "How CLI state is shared with your host" section explaining the
  bind-mount model up front so users know their host plugins/skills/
  memory carry into the container.
- Mark first-time-login section as skippable when the user is already
  authenticated on the host.
- Note the high-trust escape hatch: replace the three bind mounts with
  `type=volume` named volumes if the host/container trust boundary
  needs to be separated (Anthropic's reference pattern for enterprise).
- Replace the obsolete "rm named volume" troubleshooting row with one
  that covers EACCES/EPERM on the host-bind-mount path.

* refactor(devcontainer): address ce-code-review findings (P0 + 4 × P1 + 8 × P2 + 2 × P3)

Walkthrough resolution of the 16-finding ce-code-review on PR #1875. 15 of
16 findings applied; one (F12, Anthropic Feature floating tag) was
superseded by F6's Feature removal.

P0
- F1: WSL2 is now REQUIRED for Windows hosts, not just recommended.
  ${localEnv:HOME} resolves to empty string on Windows-native (no HOME env
  var) — bind mounts then point at /.claude, /.codex etc. and silently
  break. ensure-host-config-dirs.cjs wrote to USERPROFILE-derived paths
  via os.homedir(), so the two surfaces disagreed about which env var was
  "home" on Windows. README header reframed; "Windows 11 — WSL2 is required"
  section explains the mismatch concretely.

P1
- F2: Workspace `node_modules` volume names now include `-${devcontainerId}`
  so two GitNexus checkouts on the same host (~/work/GitNexus and
  ~/projects/GitNexus) don't share volumes and corrupt each other's
  installs.
- F3 + F5: `postCreateCommand` extracted to `.devcontainer/post-create.sh`
  with `set -euo pipefail` and six labeled echo steps so failure logs
  name the step instead of an opaque &&-chain index. Chown step extended
  to cover /home/node/.npm, /commandhistory, and /home/node/.local — these
  named-volume mount points were owned by build-time UID 1000 but the
  container's `node` is re-IDed at runtime by updateRemoteUserUID on
  non-1000 Linux hosts, leaving them unwritable until now.
- F4: Cursor installer downloaded to a temp file with curl --retry +
  --max-time; sha256 logged to build output before execution so drift
  across rebuilds is visible in CI logs. Full hard-pin (to a versioned
  downloads.cursor.com tarball with verified sha256) tracked as a
  follow-up in README "What's not included".

P2
- F6: Anthropic Feature replaced with a direct
  `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}` so
  CLAUDE_CODE_VERSION actually pins the installed binary (the Feature
  ignored the ARG and pulled latest at install time). Honors the
  earlier "pin known-good versions" decision and resolves F12's
  floating-tag concern for this Feature.
- F7: Dockerfile ARG defaults dropped for the three version vars;
  `devcontainer.json` `build.args` is now the single source of truth.
  Standalone `docker build .devcontainer/` must pass --build-arg.
- F8: ensure-host-config-dirs.cjs deleted; `initializeCommand` now uses
  POSIX `mkdir -p` + `touch ~/.gitconfig` directly, dropping the
  host-Node-on-PATH prerequisite that broke on fresh Windows+Docker
  Desktop installs without Node.
- F9: ~/.gitconfig bind-mounted read-only so `git commit` inside the
  container uses the host's user.name / user.email. Read-only so
  container-side `git config --global` doesn't leak to host.
- F10: ~/.config/gh bind-mounted (read-write) so `gh pr create` /
  `gh pr checks` / `gh issue create` work inside the container without
  re-auth. AGENTS.md's commit + PR workflow now fully functional for
  agents inside the container.
- F11: CLAUDE_CONFIG_DIR removed from Dockerfile ENV; canonical value
  lives only in devcontainer.json containerEnv. Eliminates the two-file
  edit risk.
- F13: Mounts comment now documents per-instance vs per-workspace-name
  scoping rationale so future contributors don't guess.
- F14: README "Trust boundary, concretely" paragraph names the exfil
  path explicitly (malicious npm postinstall → OAuth tokens →
  ~/.claude/projects/<workspace>/memory/MEMORY.md secrets) and lists
  vendor-side rotation runbook entries.

P3
- F15: Dockerfile pre-create + chown of /home/node/.claude, .codex,
  .cursor dropped — those paths are bind-mounted, which fully shadows
  any image-side ownership. Only .npm, .local, /commandhistory still
  benefit from the pre-create.
- F16: README "Bumping CLI versions" section rewritten against the
  post-F6 reality: CLAUDE_CODE_VERSION and CODEX_VERSION are real
  pins; CURSOR_VERSION is informational only.

Verified locally: `docker build .devcontainer/ --build-arg ...` succeeds.
Smoke-tested image: `claude --version` (2.1.153), `codex --version`
(0.134.0), `cursor-agent --version` all resolve as the non-root `node`
user; named-volume mount points (/home/node/.npm, /commandhistory) are
node-owned at build time so non-1000 host UIDs get the post-create.sh
chown fix instead of EACCES.

* fix(devcontainer): cross-platform initializeCommand + soften Windows-native posture

The previous commit's `initializeCommand` was POSIX-only (`mkdir -p $HOME/...`).
VS Code on Windows runs the host shell as `cmd.exe /c ...`, which can't
parse POSIX syntax — `$HOME` doesn't expand, `mkdir -p` errors, the init
fails with `The syntax of the command is incorrect`, and container
creation aborts before Docker is invoked.

Switch `initializeCommand` to the spec's OS-keyed object form:
- linux/darwin (covers WSL2 because VS Code runs initializeCommand in
  the WSL shell when attached via the WSL extension): POSIX mkdir+touch,
  as before
- win32: PowerShell snippet that creates the same directories under
  $USERPROFILE and touches the gitconfig if missing

Soften the README's hard "WSL2 required" framing from the previous
commit. Reality per `@devcontainers/cli read-configuration` output:
`${localEnv:HOME}` on Windows-native resolves to `C:\Users\<name>`
(VS Code falls back to USERPROFILE), so the bind mount sources are
valid Windows paths and Docker Desktop handles the translation. The
earlier `accessing specified distro mount service` failure was a
separate Docker Desktop WSL-integration issue, not a HOME-resolution
issue. Windows-native works; it's just slower with more bind-mount
permission edge cases (the husky/_/h EPERM class). The README now
explains the tradeoff and steers toward WSL2 for performance + file
watchers + permission reliability, rather than blocking Windows-native
checkouts outright.

Update the troubleshooting row to reflect the new posture.

* fix(devcontainer): Node-based initializeCommand; bind-mount .ssh + .config/git

Two fixes bundled:

1. The previous commit's OS-keyed `initializeCommand` object was based
   on a misread of the Dev Containers spec. The object form on command
   properties is **named parallel tasks**, not OS dispatch — VS Code ran
   all three keys in parallel via cmd.exe on Windows, the POSIX branches
   failed, and container creation aborted before Docker was invoked.

   Restore the single-string Node-based form:
   `node .devcontainer/ensure-host-config-dirs.cjs`. Node works
   identically in cmd.exe on Windows and bash/zsh on Linux/macOS/WSL,
   and `os.homedir()` respects $HOME on POSIX and %USERPROFILE% on
   Windows. The script is idempotent (mkdirSync recursive is a no-op
   for existing dirs; touch is gated on .gitconfig existence).

   Document Node ≥18 on the host as the only host-side prerequisite
   beyond Docker Desktop and the VS Code Dev Containers extension.
   Anyone running Claude Code on the host already has it.

2. Extend the host-bind mount surface with `~/.ssh` and `~/.config/git`,
   both read-only:

   - `~/.ssh` lets commit signing + push over SSH remotes work inside
     the container without copying private keys. Read-only mount means
     container code can read keys but can't modify or delete them.
     (Threat: a malicious dep can still read private keys from inside
     the container; the read-only mount narrows write-side blast
     radius, not read-side. Documented in the trust-boundary section.)
   - `~/.config/git` covers XDG-style git config (`~/.config/git/config`,
     `~/.config/git/ignore`, `~/.config/git/attributes`) for users who
     keep settings there instead of `~/.gitconfig`. Read-only, same as
     `~/.gitconfig`.

   Update the CLI-state-sharing table and trust-boundary paragraph to
   reflect the expanded surface.

Re-adds .devcontainer/ensure-host-config-dirs.cjs (deleted before the
OS-keyed attempt).

* fix(devcontainer): fail-fast on Windows-native with HOME-not-set diagnostic

The previous commit's "Windows-native works" softening was wrong. VS Code
on Windows-native resolves `${localEnv:HOME}` by reading the host shell's
HOME env var, and cmd.exe has no HOME set — the bind sources collapse to
`/.claude`, `/.codex`, etc., and Docker errors:

  Error response from daemon: invalid mount config for type "bind":
    bind source path does not exist: /.claude

The @devcontainers/cli output that prompted the softening was misleading
because I ran it from a Bash session with HOME already set, not from VS
Code's cmd.exe call context. The original Finding-1 P0 — that Windows-
native silently breaks the bind-mount feature — was correct.

Three changes:

1. `ensure-host-config-dirs.cjs` detects the failure mode early:
   `if (process.platform === 'win32' && !process.env.HOME)` prints a
   targeted error message naming the root cause (cmd.exe has no HOME →
   ${localEnv:HOME} resolves empty → bind sources fail) and a step-by-step
   pointer to set up WSL2. Exits 1 so VS Code surfaces it as a clean
   container-creation failure, not the cryptic Docker bind-mount error.

2. README header reverted to "Windows 11 via WSL2" only (not "and
   Windows-native"). The "Windows 11 — WSL2 is required" section names
   the specific HOME-resolution mismatch concretely so future readers
   understand why the constraint exists.

3. Troubleshooting table gets a new row for the `ERROR: GitNexus
   devcontainer requires WSL2` message pointing at the setup section.

* feat(devcontainer): support Windows-native via auto setx HOME on first run

Reverses the "WSL2 required on Windows" posture. Windows-native now
works after a one-time auto-handled setup.

The root cause of the bind-mount failure: VS Code resolves
`${localEnv:HOME}` by reading its own process env, and Windows doesn't
set `HOME` by default — Windows uses `USERPROFILE`. So the bind sources
were collapsing to `/.claude`, `/.codex`, etc., and Docker rejected them.

`ensure-host-config-dirs.cjs` now handles this automatically on Windows
hosts where `HOME` is unset:

1. Runs `setx HOME "%USERPROFILE%"`, which writes to the user-level
   Windows environment (HKCU\Environment) — no admin required. Every
   future user process inherits HOME from there.
2. Prints a clear one-time setup banner explaining the user needs to
   fully restart VS Code (File > Exit, not just close the window) for
   VS Code to pick up the new env at its next startup.
3. Exits 1 so VS Code surfaces this as a clean container-create failure
   instead of letting Docker error opaquely later.

On the second Reopen-in-Container attempt, `HOME` is now set in VS
Code's env, the script skips the setup block, creates the bind-mount
source dirs, and the container builds normally. Subsequent rebuilds
have no extra steps.

Mac, Linux, and WSL2 hosts have `HOME` set by the shell, so the new
block is a no-op there. Same `devcontainer.json` works across all
supported hosts.

README rewritten to reflect the new posture:

- Header lists Windows 11 (native) as a supported host alongside macOS,
  Linux, and WSL2, with a note that Windows-native gets a one-time
  HOME setup handled by the initializeCommand.
- New "Windows 11 setup" section walks through the auto-handled setup
  flow + a manual `setx HOME "%USERPROFILE%"` fallback for users who
  want to do it themselves.
- "Known trade-offs of Windows-native vs WSL2" subsection lays out the
  Docker Desktop Windows bind-mount edge cases (file watchers, npm
  install perf, husky/_ EPERM) so users opting into Windows-native do
  so eyes-open. WSL2 remains documented as the faster path for users
  who want it, but it's no longer the only supported one.
- Troubleshooting table gets two new rows: the one-time setup banner
  (with "what to do" instructions) and the residual `bind source path
  does not exist` case (run setx manually + fully exit VS Code).

* fix(devcontainer): drop ~/.gitconfig bind mount; defer to VS Code auto-copy

VS Code's Dev Containers extension auto-copies the host's gitconfig into
the container at attach time using `(dd ...) >> /home/node/.gitconfig`.
A read-only bind mount of ~/.gitconfig blocks that write, so attach
failed with `cannot create /home/node/.gitconfig: Read-only file system`.
Making it read-write would let the append succeed, but the bind mount
means the host file and the container file are the same file — VS Code's
append would double the host gitconfig contents on every container
start.

Drop the ~/.gitconfig bind mount entirely. VS Code's auto-copy is the
purpose-built mechanism for this, gives the container the host's
user.name / user.email transparently, and avoids both the read-only
write failure and the append-duplication trap. The container ends up
with a writable /home/node/.gitconfig that's a copy of the host's, not
a mount.

The remaining six bind mounts (.claude, .codex, .cursor, .ssh, .config/git,
.config/gh) keep their existing modes — XDG-style git config under
~/.config/git is unaffected by VS Code's auto-copy (which only targets
~/.gitconfig), so its read-only bind mount stays.

Also remove the `.gitconfig` touch from ensure-host-config-dirs.cjs
(now unnecessary) and update the README CLI-state table, sharing
explanation, and troubleshooting row to reflect that gitconfig flows
in via VS Code auto-copy rather than the bind mount.

* feat(devcontainer): bind-mount ~/.docker, ~/.aws, ~/.azure for agent workflows

Extend the host bind-mount surface so coding agents inside the container
inherit cloud + container-registry auth from the host without any
per-container setup:

- ~/.docker (read-write) — Docker registry auth (config.json) + buildx
  config. Container-registry pushes (ghcr.io, docker.io) from inside the
  container pick up host `docker login` state. Read-write because the
  Docker CLI refreshes credential-helper tokens.
- ~/.aws (read-only) — AWS CLI / SDK credentials. Read-only because
  rotating creds typically happens via the host. Empty on this dev box,
  so forward-compatible: the moment you `aws configure` on the host the
  container picks it up on the next rebuild.
- ~/.azure (read-only) — Azure CLI credentials. Same pattern as ~/.aws.

`ensure-host-config-dirs.cjs` extends to mkdir these three on init so
the bind mounts always have a valid source even if a CLI has never been
used on this host.

The Docker CLI itself isn't installed in the container by default — the
~/.docker/ mount is inert until you add `docker-outside-of-docker:1` or
similar Feature. README now calls this out under "What you still don't
have inside the container" so it's obvious which CLIs are agent-ready
and which need a feature add to become useful.

README updates:

- Bind-mount table gains a "Why" column and rows for the three new
  mounts, making it clear at a glance what each one enables.
- Trust-boundary section lists Docker registry tokens, AWS, and Azure
  creds in the read-side exfil path so the threat model stays honest as
  the credential surface grows.
- New subsection lists not-included CLIs (Docker, AWS, Azure, gcloud,
  kubectl, private-npm) with the exact Feature ID or mount snippet
  needed to enable each — turns "I want my agent to do X" into a
  one-line config change.

Verified locally: `npx @devcontainers/cli read-configuration` resolves
all 9 host bind mounts to valid C:\Users\<name>/* paths on Windows.

* refactor(devcontainer): hybrid AI CLI config — read-only host share + per-container credentials

Restructure the Claude Code / Codex / Cursor mount topology to fix the
silent first-run-UI bug surfaced in PR testing, and to harden against
the host-write-through escape class the previous bind-mount design
exposed.

The actual root cause of the first-run wizard firing on the user's
screenshot — confirmed via three parallel research agents (best
practices, framework docs deep dive of the OpenAI Codex Rust source,
adversarial design review) — was NOT a credential permission check.
Claude Code splits state across `~/.claude/.credentials.json` AND
`~/.claude.json` (a FILE at $HOME, sibling of the `.claude/` dir).
The latter holds `hasCompletedOnboarding`, `userID`, `oauthAccount`
metadata, MCP user-scope config, and per-project trust state — and
Claude Code reads it at literal `$HOME/.claude.json`, not via
`CLAUDE_CONFIG_DIR`. The previous design mounted `~/.claude/` but
left `~/.claude.json` outside the topology entirely, so every container
started with a missing onboarding-state file and re-ran the wizard.

Confirmed by tfvchow/field-notes-public#10:
"Persisting .credentials.json alone is NOT sufficient. Without
.claude.json, Claude Code treats the session as a fresh install and
prompts for login regardless of valid credentials being present."

The new topology:

**Mounts**
- `${localEnv:HOME}/.claude` → `/host/.claude` (read-only bind)
- `${localEnv:HOME}/.codex` → `/host/.codex` (read-only bind)
- `${localEnv:HOME}/.cursor` → `/host/.cursor` (read-only bind)
- `${localEnv:HOME}/.claude.json` → `/host/.claude.json` (read-only bind)
- `claude-config-${devcontainerId}` → `/home/node/.claude` (named volume)
- `codex-config-${devcontainerId}` → `/home/node/.codex` (named volume)
- `cursor-config-${devcontainerId}` → `/home/node/.cursor` (named volume)

**containerEnv** gains `CODEX_HOME=/home/node/.codex` (Codex's own env
override, per its public Rust source). `CLAUDE_CONFIG_DIR=/home/node/
.claude` was already set.

**`post-create.sh`** stages the named volumes on first run:

- Symlinks shareable subdirs from `/host/.claude` into the named volume:
  `plugins/`, `skills/`, `agents/`, `memory/`, `commands/`. Codex gets
  `config.toml` symlinked. Cursor has no shareable subdirs (cli-config
  .json conflates auth and settings).
- Copies `.credentials.json`, `auth.json`, `cli-config.json` on first
  run with `chmod 600`. After first run, container manages its own
  refresh; host's credentials untouched.
- Copies `~/.claude.json` on first run (with stub
  `{"hasCompletedOnboarding":true,"installMethod":"global"}` fallback
  for hosts that haven't run Claude Code). This is the fix for the
  observed onboarding-wizard loop.

`ensure-host-config-dirs.cjs` now also touches `~/.claude.json` on the
host if missing, so the bind mount has a valid source on hosts that
have never run Claude Code.

**Why read-only + named volume vs. the previous full bidirectional
bind mount:**

1. **Host filesystem write-through escape, eliminated.** Previous
   design symlinked `plugins/`, `agents/`, `skills/` write-through
   into the host's `~/.claude/` — a malicious npm package in the
   workspace dep tree could drop `agents/evil.md` into the host's
   config, which the next host Claude session would auto-load. The
   read-only `/host` mount blocks this; container compromise no
   longer persists across teardown via host-side autoload.
2. **Windows bind-mount perm-flattening, sidestepped.** Files
   surfaced through a Docker Desktop Windows bind mount appear as
   `root:root` mode `777`. Credentials in the named volume come with
   proper Linux ownership and `chmod 600` — what each CLI expects on
   write (none enforces on read, but write-side hygiene matters for
   the host's understanding of "where credentials live").
3. **No `ide/` lock-file collisions.** Previous design symlinked
   `~/.claude/ide/` write-through, including per-PID lock files. Host
   PID and container PID namespaces are unrelated → lock-file PIDs
   misclassify dead processes as alive. Skipping `ide/` keeps lock
   files container-local.
4. **No `projects/` ghost dirs.** Host encodes the workspace path as
   `D--development-coding-GitNexus`, container as `-workspace`.
   Bidirectional `projects/` symlinks would split memory and session
   state across two ghost project dirs for what is conceptually the
   same project. Skipping `projects/` keeps per-project state
   container-local; host's projects/ stays untouched.
5. **No `settings.json` version drift.** Container is pinned to a
   specific Claude Code version (`CLAUDE_CODE_VERSION` build arg);
   host floats with auto-update. Bidirectional `settings.json` writes
   produced silent schema rollback. Skipping settings.json keeps each
   side authoritative for its own version.

**README** rewritten in the same section to describe the new topology
honestly: what's shared, what isn't, the OAuth refresh-token
divergence between host and container, per-CLI quirks (macOS Keychain
storage, Cursor's known upstream in-container auth bug, Codex
keyring storage). Trust-boundary section updated to name the threat
model accurately — same read surface as before (malicious dep can
still READ all credentials), but write-through into host plugin/agent
dirs is now blocked.

Verified locally: `@devcontainers/cli read-configuration` resolves all
19 mounts correctly on Windows, `post-create.sh` parses, and
`ensure-host-config-dirs.cjs` idempotently touches `~/.claude.json`.

Research backing this design:

- Anthropic Claude Code devcontainer docs (named-volume pattern):
  https://code.claude.com/docs/en/devcontainer
- tfvchow/field-notes-public#10 (both files required):
  https://github.com/tfvchow/field-notes-public/issues/10
- anthropics/claude-code#29029 (VS Code extension strips
  hasCompletedOnboarding):
  https://github.com/anthropics/claude-code/issues/29029
- OpenAI Codex Rust source (no read-side perm check):
  https://github.com/openai/codex/blob/main/codex-rs/login/src/auth/storage.rs
- Cursor CLI in-Docker auth issue:
  https://forum.cursor.com/t/cursor-agent-authentication-issue-inside-docker/143995

* fix(devcontainer): resync AI CLI state from host on every container-create

Two bugs were causing Claude Code to fire the onboarding wizard inside the
container even with valid host credentials:

1. Missing the second state file. Claude Code 2.1.x writes a small `.claude.json`
   INSIDE `CLAUDE_CONFIG_DIR` (carrying migration tracking + userID), not just the
   one at `$HOME/.claude.json`. If the userIDs in the two files disagree, Claude
   treats the session as inconsistent and re-onboards. The previous post-create.sh
   only copied the `$HOME` one.

2. First-run guards (`[ ! -e $dst ]`) skipped the copy when stale named volumes
   from earlier rebuilds still had the prior session's state in them, leaving the
   container desynced from the host.

Replace `copy_on_first_run` with `sync_from_host` that always overwrites from
host on container-create. `link_readonly_share` now clears stale non-symlink dst
entries before linking. Copies both `$HOME/.claude.json` and
`$CLAUDE_CONFIG_DIR/.claude.json` so userIDs stay aligned. Container can still
mutate its own state between rebuilds; resync only happens on rebuild
(postCreate boundary).

* docs(devcontainer): document sync-from-host design + dual-source auth flow

README still described the old "first-run copy" behavior. After the
post-create.sh change to always-sync-from-host, the design works either
direction:

- Log in on host → next container-create syncs the credentials into the
  named volume.
- Log in inside the container → the named volume persists the login across
  rebuilds; the host has no source to overwrite from, so it stays alone.

Also documents the two-Claude-state-files trap (`$HOME/.claude.json` AND
`$CLAUDE_CONFIG_DIR/.claude.json`, both with the same userID required), and
the volume-deletion recovery path for stale named volumes carried over
from earlier rebuilds.

* fix(devcontainer): full plugin/config parity by dropping CLAUDE_CONFIG_DIR + syncing settings.json

Two changes that together give the container the same plugins and configs
as the host for all three AI CLIs (login stays per-container):

1. Drop CLAUDE_CONFIG_DIR from containerEnv. The named-volume mount target
   `/home/node/.claude` already matches Claude's default `~/.claude`, so
   the env var added no behavior — but setting it changed which file
   Claude reads `hasCompletedOnboarding` from. With it set, Claude reads
   `$CLAUDE_CONFIG_DIR/.claude.json` (the small identity-only file that
   does NOT carry `hasCompletedOnboarding`); without it, Claude reads
   `$HOME/.claude.json` (the big onboarding-state file that does). The
   wizard fires every container-create when set, skips when unset.

2. Sync `settings.json` from host (Claude) + symlink `memories/` and
   `skills/` from host (Codex). Theme + `enabledPlugins` +
   `extraKnownMarketplaces` live in `settings.json` — without syncing
   it, the theme picker fires and host-installed plugins stay disabled
   even though their files are symlinked in. Codex's `memories/` and
   `skills/` are the symmetric Codex user-installed surface, now shared
   the same way Claude's plugins/skills/agents/memory/commands are.

Cursor stays as-is — `cli-config.json` conflates auth+settings (already
synced), and there's no separate plugin surface to mirror.

Login details remain per-container by design (acceptable to re-login on
rebuild). Everything else — plugins, skills, agents, memory, MCP user-
scope config, project trust, theme, plugin enablement — now matches
host on every container-create.

* refactor(devcontainer): hybrid RW bind + per-container creds — fixes EROFS on in-container plugin install

The previous Option B topology (RO host stage + named volume + symlinks
into the volume) made `/plugin marketplace add` inside the container fail
with EROFS — the symlinks pointed at a read-only mount, so Claude
couldn't create new marketplace dirs. Switch to a hybrid: shareable
content (plugins/skills/agents/memory/commands/settings.json/$HOME/.claude.json
for Claude; config.toml/memories/skills for Codex) gets a direct RW bind
from host so reads and writes go bidirectionally; credentials + the
small identity file stay in per-container named volumes so logout in
container doesn't log out host.

Mount precedence does the heavy lifting: the named volume mounts at
/home/node/.<cli> first, then sub-path bind mounts overlay specific
sub-paths. Container's view at /home/node/.claude/plugins/ is the host
dir; container's view at /home/node/.claude/.credentials.json is the
named volume's file.

What this gives you:
- /plugin marketplace add in container = installed on host
- New skill on host = visible in container immediately (no rebuild)
- claude logout in container = host stays logged in
- compound-engineering plugin enabled on host = enabled in container
- Theme picker fires once (or never if host has theme set)

What it costs:
- Write-through: a compromised npm dep in workspace deps can write to
  host ~/.claude/{plugins,skills,agents,memory,commands}/. Documented
  trade-off; for personal dev, accepted. Credentials still per-container.

post-create.sh becomes much simpler — only syncs the four credential
files from host into the named volumes. No more symlink dance, no more
state-file merging.

ensure-host-config-dirs.cjs gains the new bind sources: the shareable
subdirs and settings.json/config.toml files get mkdir/touched on host
so Docker doesn't reject the mount when a CLI has never been used.

* fix(devcontainer): translate host plugin registry paths to Linux on rebuild

The previous topology bind-mounted the entire `~/.claude/plugins/`
directory from host. That brought through plugins, marketplaces, and
extracted cache content correctly — but ALSO brought through the
registry JSONs (`known_marketplaces.json`, `installed_plugins.json`,
`plugin-catalog-cache.json`) which carry absolute OS-native paths:

  "installLocation": "C:\Users\gergo\.claude\plugins\marketplaces\X"
  "installPath": "C:\Users\gergo\.claude\plugins\cache\Y\Z"

Claude in the Linux container fails to resolve these Windows paths and
reports `Marketplace X failed to load: cache-miss`.

Split the topology:
- `plugins/marketplaces/` (git clones) and `plugins/cache/` (extracted
  plugin files) stay bidirectional RW binds — content is path-independent.
- Registry JSONs move into the per-container named volume. post-create.sh
  reads host's versions, rewrites any absolute path ending in
  `/.claude/plugins/<rest>` (Windows `C:\Users\...` and POSIX
  `/Users/...` / `/home/...` patterns) to `/home/node/.claude/plugins/<rest>`,
  and writes the translated result to the volume.

What this gets you:
- Plugin installed on host → next container rebuild has it (translated).
- Plugin installed inside container → lives in volume registry; lost on
  rebuild (consistent with credentials model). Re-install on host for
  persistence.

ensure-host-config-dirs.cjs now also creates `plugins/marketplaces/` and
`plugins/cache/` on host if absent (Docker rejects bind mounts whose
source doesn't exist).

* fix(devcontainer): clean stale plugin/skill symlinks from prior design before writes

A user upgrading from Option B (read-only host stage + symlinks) to the
current hybrid RW-bind topology hit EROFS in post-create.sh when the
plugin registry path-translator tried to write
`/home/node/.claude/plugins/known_marketplaces.json`. The named volume
still carried `/home/node/.claude/plugins -> /host/.claude/plugins`
(Option B's symlink). The new design's sub-path bind mounts at
`plugins/marketplaces` and `plugins/cache` overlay through the symlink,
but writes to the parent dir itself resolve via the symlink to the RO
host stage and fail.

Drop any leftover symlinks at known target paths early in step 2 so the
mkdir/writes that follow land in the volume.

* refactor(devcontainer): split workspace-deps to updateContentCommand

post-create.sh was doing two unrelated jobs: workspace dependency install
(four `npm install` runs in topological order) and AI CLI credential
sync. They have different lifecycle needs — deps should re-run when
lockfiles change, AI sync should run once per container — but both were
gated on container-create.

Per Dev Container spec lifecycle, `updateContentCommand` is the right
hook for workspace deps: runs at container-create AND on content
changes (lockfile updates). `postCreateCommand` is right for AI CLI
sync: container-create only.

Move steps 3-7 (husky cleanup + four `npm install` runs) into
install-deps.sh wired as `updateContentCommand`. Split the chown step
too — install-deps owns workspace-side dirs (node_modules volumes,
~/.npm), post-create owns AI-side dirs (~/.claude, ~/.codex, ~/.cursor,
/commandhistory, ~/.local). Each script now has one concern.

post-create.sh drops from ~187 lines to 148; install-deps.sh is 56 lines
new. Faster rebuilds when nothing about deps changed (the credential
sync + path translation work still runs every container-create, but the
npm install dance no longer does).

Research backing (no other simplification applies):
- Anthropic's reference devcontainer uses pure named volumes; no
  host-state inheritance pattern is published.
- Path translation has no upstream fix (issues #21916, #10379 closed
  without resolution). Our Node rewrite is the workaround.
- pnpm workspaces (`pnpm -r install`) would replace the four installs
  with one command, but that's a real refactor (touches
  gitnexus/scripts/build.js + 4 package.json files); deferred.
- `HUSKY=0` in containerEnv would drop the `rm -rf .husky/_` hack, but
  would also stop pre-commit hooks from firing inside the container;
  deferred.

* fix(devcontainer): drop single-file binds — fixes Codex `batchWrite failed in TUI`

On Docker Desktop Windows the named volumes are ext4 (`/dev/sdd`) while
single-file bind mounts from the Windows host land as 9p (drvfs).
Different filesystems → atomic config writes (write `foo.tmp`, then
rename onto `foo`) trip EXDEV `inter-device move failed` /
`Device or resource busy`.

Codex's TUI surfaces this as `config/batchWrite failed in TUI` when
saving model preference. Claude's writes to settings.json / .claude.json
fail the same way, silently.

Reproduction in container:
  $ echo x > /tmp/foo.toml; mv /tmp/foo.toml /home/node/.codex/config.toml
  mv: inter-device move failed: ... Device or resource busy

Fix: drop the three single-file bind mounts. Sync host's versions into
the named volume on container-create via `sync_from_host` (same pattern
already used for credentials). Atomic rename within the volume works
because everything is ext4.

Trade-off: container writes to these files no longer propagate to host;
they stay in the volume until next rebuild, which re-syncs from host.
Host is source of truth on rebuild — same model as credentials. Plugin/
skill/agent/memory/command DIRS still bind-mount bidirectionally (atomic
writes within a dir bind stay on one filesystem, no EXDEV).

Files affected:
- ~/.codex/config.toml
- ~/.claude/settings.json
- ~/.claude.json (HOME-level — added `/host/.claude.json` RO mount back
  for sync_from_host to read)

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

* feat(devcontainer): Codex + Cursor plugin/config host parity with Claude

Codex plugins installed in the container never reached the Windows host
because, unlike Claude, the Codex plugin tree wasn't bind-mounted —
only memories/ and skills/ were. Verified via live /proc/mounts: Claude
binds 6 shareable dirs (incl. plugins/marketplaces + plugins/cache),
Codex bound 2. So `codex plugin add` wrote into the ext4 named volume
and stayed there.

Codex changes:
- Bind the WHOLE ~/.codex/plugins dir + ~/.codex/prompts (plus existing
  memories/skills). Strace of two real `codex plugin add` runs proved
  the installer stages INSIDE plugins/cache/<marketplace>/ and renames
  intra-dir, so a single 9p bind of plugins/ keeps the rename intra-fs —
  no EXDEV (the bug that broke single-file binds). .tmp/ stays on the
  volume (it's the cross-fs staging source). No path translation needed:
  Codex enablement lives in config.toml as git URLs, not FS paths.
- Verified live: `codex plugin add compound-engineering@...` now writes
  through to C:\Users\...\.codex\plugins\cache\ on the Windows host, and
  host-created files appear in the container (bidirectional).

Cursor changes (review found cursor-agent has a real plugin surface, not
editor-only — Cursor 2.5 Marketplace shared by IDE + CLI):
- Bind plugins/marketplaces, plugins/local, rules, commands, agents,
  skills (dir binds, EXDEV-safe).
- Copy-on-create mcp.json (single file → EXDEV-unsafe as bind), alongside
  the existing cli-config.json.
- Translate plugins/installed_plugins.json (carries absolute Windows
  paths like Claude's) — generalized the existing path-rewrite to run
  for both Claude and Cursor.
- hooks.json deliberately NOT shared (runs shell commands → supply-chain
  surface); documented as opt-in.

ensure-host-config-dirs.cjs pre-creates all new host bind sources.
post-create.sh defensive symlink cleanup extended to the new Codex/Cursor
paths. README updated with the accurate per-CLI share/sync/translate
matrix.

Design adversarially verified (straced installs, EXDEV primitive tests,
sqlite-under-bind check, path-encoding check) before implementing.

* fix(devcontainer): resolve ce-code-review findings (doc drift, chown scope, .cjs extraction, CI smoke)

Multi-agent review (9 reviewers) found the devcontainer files carried
comments + README from the abandoned read-only-symlink design, plus real
behavioral gaps. Resolved all actionable findings (no deferrals).

Documentation drift (the headline — stale comments described a security
model opposite to what shipped):
- README "Trust boundary" claimed a malicious dep "cannot write back …
  the read-only /host mount blocks the write." FALSE — the shareable dirs
  are RW-bound. Rewrote to document the bidirectional write-through, what
  stays one-way (credentials never flow back), and how to close it.
- devcontainer.json mount group-1 comment described "selectively symlinks
  … read-only eliminates write-through" — replaced with the RW-bind reality.
- Header "Windows-native is unsupported" -> supported (auto HOME setup).
- containerEnv comment "credentials persist in host-bind-mounted dirs" ->
  they live in the named volumes.
- hooks.json exclusion documented honestly as a partial mitigation, not a
  clean boundary (commands/agents/skills/rules are equally executing).
- ~/.local "named volume" -> image directory.

Behavioral fixes:
- chown -R recursed into the RW host binds (could rewrite host ownership /
  EPERM-abort provisioning on non-UID-aligned Linux). Switched to
  `find -xdev` per dir so chown stays on the volume filesystem.
- Cursor installer wrapped in `timeout 300` — its inner binary download
  isn't covered by curl --max-time and could hang docker build forever.
- Removed dead CURSOR_VERSION ARG/ENV/build-arg (never consumed; "latest"
  implied a pin the installer can't honor). Documented why Cursor is unpinned.

Extraction + tests (the two inline post-create.sh node heredocs were
unlintable and untestable; the path regex had had bugs):
- seed-claude-config.cjs — installMethod-strip seed, now with a non-object
  guard (a bare-value/array host .claude.json could otherwise slip the
  try/catch and silently re-trigger onboarding) and labeled write errors.
- translate-plugin-registries.cjs — plugin-registry path translation with
  labeled errors.
- translate-plugin-registries.test.cjs — 12 tests (Windows/POSIX paths,
  cross-CLI isolation, nested objects, non-object/empty-config guard).
- post-create.sh calls the modules via $SCRIPT_DIR.

CI:
- .github/workflows/ci-devcontainer.yml — runs the unit tests + shell
  syntax checks + a `@devcontainers/cli build` smoke on .devcontainer/**
  changes. Conforms to the repo concurrency convention (validator passes).

Documented (real gaps, fixes are honest docs since no correct auto-fix
exists): user-scope MCP servers with absolute host command paths don't
resolve in-container; user-scope config is copy-on-create so host edits
need a rebuild; in-container plugin installs get shadowed by an empty host
bind on rebuild (recovery noted); plugin installs are single-writer across
checkouts; gh/docker RW-vs-ssh/aws/azure-RO rationale.

Verified: fresh `@devcontainers/cli up` succeeds; installMethod stripped,
registry translated to Linux paths, credentials node:node, 12/12 tests pass.

* fix(devcontainer): set persist-credentials:false on CI checkouts + prettier

- zizmor `artipacked` (CodeQL/GitHub Advanced Security) flagged both
  actions/checkout steps in ci-devcontainer.yml: checkout defaults to
  persist-credentials:true, leaving GITHUB_TOKEN in .git/config where it
  can leak into uploaded artifacts. Both jobs are read-only (run tests /
  build smoke, never push), so persist-credentials:false is correct —
  matches the repo convention in codeql.yml / ci-tests.yml.
- Ran prettier 3.8.0 over the new .cjs modules + test (single-quote/style
  normalization to match the repo). JSON/YAML were already compliant;
  README is in .prettierignore; .sh has no prettier parser. Behavior
  unchanged — 12/12 transform unit tests still pass.

* fix(devcontainer): resolve adversarial review findings (pins, RO mounts, tests)

Resolves the blocking + actionable findings from the PR #1875 review:

- Pin base image by digest as bare name@digest [#1]. The :tag@digest form
  trips the @devcontainers/cli image-name parser (which builds this image
  in CI and in VS Code "Reopen in Container"); bare name@digest is the
  parser-compatible form. Verified by a full local build.
- Pin Cursor by version + per-arch sha256 and fetch the artifact directly
  instead of executing cursor.com/install; fail-closed on mismatch [#2].
- Mount ~/.config/gh and ~/.docker read-only so a compromised dep can't
  rewrite the host GitHub token / Docker credHelper [#4].
- Pin @devcontainers/cli@0.87.0 in the CI smoke [#5].
- chown via find -xdev in install-deps.sh (symlink-safe; matches
  post-create.sh) [#6].
- Add filesystem-I/O tests (translate/readHostConfig/seed main/ensurePaths)
  and refactor ensure-host-config-dirs to be unit-testable [#7].
- Stop pre-creating settings.json/config.toml on the host; only the real
  single-file bind source (.claude.json) is touched [#10].
- Add a prominent top-of-README security callout for the RW write-through
  trade-off and reframe the deferred egress firewall as the key missing
  compensating control [#3, #9].

Full devcontainer build verified locally (digest pull + pinned Cursor
download/extract/symlink). 24/24 config-transform tests pass.

* fix(devcontainer): resolve local adversarial-review findings (low/nit)

Follow-up to a local branch review (run after the cloud review crashed before
producing findings); all 5 confirmed findings were low/nit:

- chown via `find -xdev -exec chown -h`: add -h so chown acts on a symlink
  ITSELF, not its target. Without it a dangling node_modules/.bin link aborted
  provisioning under `set -e`, and a cross-fs symlink target could be
  dereferenced/rewritten. Verified in a clean container (regular files still
  chowned; dangling link no longer aborts; cross-fs target untouched). Applied
  to install-deps.sh and post-create.sh; the inline comments are corrected to
  describe -xdev (descent bound) and -h (no deref) as the two distinct guards.
- Reword the .cjs header claims from "lintable" to "unit-tested and
  prettier-checked": ESLint applies no rules to .cjs in this repo; CI only
  prettier-checks them.
- README: the initializeCommand is `node ensure-host-config-dirs.cjs`, which
  creates the full bind-source set, not a bash `mkdir -p` of four dirs.
- ci-devcontainer.yml: document that the x64 runner exercises only the amd64
  Cursor branch; the arm64 sha/URL is hash-pinned (verified against the
  published artifact) but not built in CI.
- Make the seed chmod-644 test meaningful: pre-create dst at 0o600 so only the
  explicit chmodSync can widen it (the prior assertion passed under the default
  umask regardless of whether the chmod ran).

25/25 config-transform tests pass; arm64 + x64 Cursor artifacts verified.

* docs(devcontainer): rewrite code comments in plain English

The devcontainer comments had grown dense and jargon-heavy. Rewrite them
across all 9 files into short, plain-English sentences — same facts and
reasoning, just clearer wording.

Comments only; no code changed. Verified: the diff touches comment lines
only, 25/25 config-transform tests pass, devcontainer.json is still valid
JSONC with build.args + readonly mounts unchanged, shell scripts pass
`bash -n`, and prettier is clean.

* feat(devcontainer): persist AI CLI session state across container recreation

Add dedicated per-workspace named volumes (mount group 6) for the three
AI CLIs' session/resume state so `claude --resume`, `codex resume`, and
`cursor-agent resume` survive a rebuild, a full delete-and-recreate, and
the `docker volume rm <cli>-config-*` re-login fix:

  - Claude -> ~/.claude/projects
  - Codex  -> ~/.codex/sessions
  - Cursor -> ~/.cursor/chats + ~/.cursor/projects

The volumes are SEPARATE from the credential/config volumes and keyed
like the node_modules volumes (${localWorkspaceFolderBasename}-...-
${devcontainerId}), so wiping a config volume to force a re-login no
longer destroys session history. Session state already survived a plain
rebuild (it lived in the config volume); this closes the recreation,
volume-rm, and devcontainerId-change gaps.

Kept container-private (not host bind mounts) deliberately: transcripts
can contain pasted secrets, so a host bind would spill them to host
disk, widen the supply-chain write-through surface, and leak
cross-project transcripts. A commented-out opt-in host-bind block is
included for users who accept that trade-off.

post-create.sh: chown each new volume root explicitly (find -xdev stops
at the config-volume filesystem boundary and won't descend into them),
guarded with `[ -d ] || continue` so a missing root can't abort
provisioning under set -e.

README: document the topology, what survives vs not, the one-time
first-rebuild masking of pre-existing config-volume sessions, updated
rebuild/reset commands, and the trust-boundary impact.

* feat(devcontainer): isolate host AI-CLI config via seed-once copies + persist claude-mem

Replace the read-write host bind mounts for the AI-CLI shareable dirs
(Claude skills/agents/memory/commands/plugins; Codex plugins/prompts/
memories/skills; Cursor rules/commands/agents/skills/plugins) with a
seed-once copy from a read-only /host/.<cli> stage into the per-container
config volume. The container gets its own writable copy and can never
write back to the host, closing the write-through vector where a
compromised in-container dependency could drop a malicious agent, command,
skill, or plugin onto the host for the next host session to auto-load.

Add a per-container claude-mem named volume (claude-mem-${devcontainerId})
at /home/node/.claude-mem, seeded once from a read-only /host/.claude-mem
stage. claude-mem's multi-GB SQLite + Chroma store is kept off a host bind
(unreliable fcntl locking / corruption risk over 9p on Docker Desktop
Windows) while still surviving rebuilds.

- post-create.sh: seed shareable dirs (marker-gated, seed-once) and run
  plugin-registry translation per seeded CLI; seed claude-mem behind a
  completion-sentinel guard that self-heals an interrupted multi-GB copy;
  chown the claude-mem volume only on first create.
- translate-plugin-registries.cjs: add selectRegistries() so translation
  runs per-CLI seed-once instead of clobbering container-installed plugins.
- ensure-host-config-dirs.cjs: add ~/.claude-mem; drop the shareable
  subdirs (no longer bind sources).
- devcontainer.json: drop the RW shareable binds; add the claude-mem
  volume + read-only stage.
- README: rewrite trust-boundary, mount table, and rebuild/reset docs for
  the copy model.
- tests: cover selectRegistries and the trimmed DIRS (30 pass).

* feat(devcontainer): add Bun 1.3.14, pinned via build arg

Installed by the official bun.sh/install script with the release tag
passed as the first positional arg, so the version is pinned even though
the install path itself is an unverified remote script (the one such
exception in the image — Cursor and the base image stay sha256/digest-
pinned). BUN_INSTALL is set in ENV so the binary lands at a known path
and the installer's rc-file edits don't matter. unzip is added to apt
since the Bun installer extracts a .zip.

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

* feat(devcontainer): persist gh auth via copy-into-volume model

Move ~/.config/gh from a read-only bind to the same read-only host
stage + per-container named volume pattern used for the AI CLI
credentials. post-create.sh seeds hosts.yml/config.yml from the
/host/.config/gh stage into the gh-config volume on create, so an
in-container `gh auth login` now persists across rebuilds while the
read-only stage still prevents any write-back to the host token.

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

* fix(devcontainer): bump Claude Code to 2.1.156 for Opus 4.8

The pin was 2.1.153, which predates Opus 4.8 support (added in
2.1.154). With DISABLE_AUTOUPDATER=1 the container never updated past
the pin, so Claude Code only offered models up to 4.7. Bump to the
latest 2.1.156 so Opus 4.8 is available.

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 05:09:01 +01:00
Sparsh 4f7697c43b fix: COBOL parsing-layer coverage gaps — F17-F23 (#1925) (#1959) 2026-06-01 21:08:07 +01:00
Gergő Magyar 0fc0211d26 fix(ingestion): migrate all languages' inheritance to scope-resolution on the worker path (#1951) (#1956) 2026-06-01 17:04:27 +01:00
Zander RaycraftandGergő Magyar fca30c7e26 fix(audit): Centralize heritage supertype matching (#1921/#1922) (#1940)
* fix(audit): Centralizes heritage supertype matching so qualified, generic, scoped, and interface bases produce inheritance edges across all OO languages, with per-language configs and fixtures.

* fix(audit): Harden parsing for #1922 with per-parse timeouts, ERROR/partial parse flags, tree-sitter pinned to 0.21.1, and CI ABI checks for every grammar.

* fix: action lint passing

* fix: feedback from triage review

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-01 13:16:17 +01:00
TuZaaaaaandwujuntong_a ce7f45e18d docs: document embeddings node limit options (#1961)
Co-authored-by: wujuntong_a <wujuntong_a@aspirecn.com>
2026-06-01 10:32:33 +01:00
070c3d5154 fix(ingestion): skip File->Member DEFINES edges for class members (#1949)
* fix(ingestion): skip File->Member DEFINES edges for class members

Previously every symbol — including Methods and Properties that belong
to a class — received a direct File->Symbol DEFINES edge. This caused
the radial-layout view to show File linking directly to all Properties
and Methods, bypassing their enclosing class node and flattening the
OOP hierarchy.

Fix: gate the DEFINES emission on the symbol having no owner. Class
members are already reachable through the File->Class DEFINES edge and
the Class->Member HAS_METHOD / HAS_PROPERTY edges, so the redundant
direct edge is unnecessary and misleading in the graph.

The same guard is applied in all four emission sites:
- parsing-processor.ts  (sequential fallback path)
- parse-worker.ts       (main symbol loop + routed-property branch)
- call-processor.ts     (routed-property branch)

Pipeline-graph golden updated: DEFINES 21→20, relationships 74→73.

Closes #1944

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* fix(wiki): extend getFilesWithExports to include exported class members

The PR that removed File→Member DEFINES edges left getFilesWithExports()
under-reporting: its one-hop MATCH only reaches top-level symbols. Add a
UNION leg that follows File→DEFINES→Class→HAS_METHOD/HAS_PROPERTY→Member
so exported class methods and properties appear in wiki/cluster export
summaries again.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-01 07:43:55 +01:00
Gergő MagyarandClaude Opus 4.8 e275826236 fix(csharp): eliminate global-namespace typeBindings O(files²) OOM (#1871) (#1954)
* fix(csharp): eliminate global-namespace typeBindings O(files²) OOM (#1871)

Large C# solutions with tens of thousands of files in the global
(unnamed) namespace OOM'd / hung for hours at "Resolving types
(Csharp 2/3)". PR #1905 fixed the BindingRef twin of this via the
`workspaceFqnBindings` fast-path, but left the typeBindings
propagation loop in `populateCsharpNamespaceSiblings` untouched: it
copies every global file's module-scope return-type bindings into
every OTHER global file's `Scope.typeBindings`. With S files in the
`''` bucket and K distinct method names, that is O(S²) time and
O(S·K) memory — ~1.3B Map entries (~65-130 GB) at 36k files.

Measured on a concentrated global-namespace fixture: the per-file
copy went quadratic (1000→2000 files = 3.06× for 2× the files,
65s at 2000). Route global-namespace module typeBindings through a
new scope-independent `workspaceTypeBindings` channel populated ONCE
(O(K)) and consulted as a fallback by the typeBindings chain-walkers
(`findReceiverTypeBinding`, `followChainPostFinalize`), instead of the
per-file copy. After: 2000 files 6.3s, 4000 files 6.8s, heap linear.

This also makes resolution MORE correct, not just faster. The C#
spec makes the unnamed namespace a single declaration space whose
members are "available for use in a named namespace", so global types
are visible from every file. The old per-file copy only exposed them
to OTHER no-namespace files; named-namespace files never saw them.
Consulting the shared channel from every scope chain mirrors how
Roslyn resolves against a single `Compilation.GlobalNamespace` symbol
rather than copying symbols per file.

Strengthen csharp-pipeline-benchmark.test.ts so it would catch this:
give each file a unique method name (a shared name collapses the
module-typeBinding key and skips all copies, hiding the blow-up) and
raise the concentrated scales to 2000 so the sub-quadratic assertion
trips on the regression.

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

* fix(csharp): generalize shared-channel resolution to concentrated named namespaces (#1871)

#1954 eliminated the namespace-siblings O(files²) OOM only for the global
('' / no-declared-namespace) bucket. A solution with all files under one
named namespace (e.g. file-scoped `namespace Company.Product;`, common in
modern .NET) still reproduced the #1871 blow-up — and in BOTH loops: the
BindingRef per-scope augmentation (#1905's twin) AND the typeBindings
per-file copy (#1954's twin) were each still O(N²) for a named bucket.

Generalize the shared-channel approach to named namespaces:
- Add namespace-keyed channels `namespaceFqnBindings` / `namespaceTypeBindings`
  (the per-namespace analogues of `workspaceFqnBindings` / `workspaceTypeBindings`)
  plus `accessibleNamespacesByScope`, populated ONCE per named bucket from the
  existing `expandedNamespaces` derivation — O(defs), not O(files × defs).
- Make the shared walkers (`findReceiverTypeBinding`, `lookupBindingsAt`,
  `followChainPostFinalize`) namespace-aware: after the per-scope chain and the
  flat global channel miss, consult the per-namespace channels gated by the
  caller module's accessible namespaces. Language-neutral — only the C# hook
  populates the channels; the machinery names no language (AGENTS rule).
- Precedence preserved: local chain → named namespace → global. Named is
  consulted before the flat global channel because pre-#1871 named siblings
  lived in the chain / bindingAugmentations (above the workspace channel), so a
  name in both a named and the global namespace must still resolve named-first.
- `using static` member exposure and the global '' fast-paths are unchanged.

Parity-neutral: `run-parity.ts --language csharp` passes (legacy DAG ==
registry-primary, 218 tests each); the C# resolver suite (386 tests) is green.
Measured: a concentrated named namespace at 500/1000/2000 files now scales
linearly (~0.57×) and ~5.6s at 2000 files, vs the quadratic blow-up before.

Tests:
- New always-run unit coverage for the walker fallbacks
  (namespace-channel-lookup.test.ts): global `workspaceTypeBindings` (the #1954
  channel previously covered only by a gated benchmark), namespace gating /
  no-leak, named-before-global precedence, local shadowing, loop termination.
- Extend the immutability validator + invariant I8 to the new channels and
  `workspaceTypeBindings`; update the `mkIndexes` factory.
- Add a concentrated-NAMED-namespace shape to the C# pipeline benchmark with
  the sub-quadratic scaling assertion and an edge-count sanity check.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:21:07 +01:00
7d40156003 feat(swift): migrate Swift to scope-based registry resolution (#937) (#1948)
* feat(swift): migrate Swift to scope-based registry resolution (#937)

Ring 3 of RFC #909 — Swift is the final language migrated to the
scope-based registry resolution pipeline. Flips Swift into
MIGRATED_LANGUAGES so registry-primary call resolution is the production
default, with dual-mode parity proven: the resolver suite passes 77/77
under both the legacy DAG (REGISTRY_PRIMARY_SWIFT=0) and the
registry-primary path (REGISTRY_PRIMARY_SWIFT=1).

New language module src/core/ingestion/languages/swift/ (mirrors csharp/):
query, captures, interpret, import-decomposer, receiver-binding,
signature-bindings, arity (+metadata), merge-bindings, simple-hooks,
import-target, target-siblings, implicit-imports, sibling-type-bindings,
scope-resolver, cache-stats, index. Parse-time hooks wired into the
existing flat languages/swift.ts (coexists with the swift/ dir, like
kotlin) and the resolver registered in the scope-resolution registry.

tree-sitter-swift 0.7.1 specifics handled in the Swift module (not in
shared code):
- class / struct / extension all parse to class_declaration; extensions
  are re-keyed onto the extended type so members hoist (like C# partial).
- if-let / guard-let have no if_let_binding node — the optional binding
  is synthesized from if_statement / guard_statement.
- the name: field is reused for func name, param labels, param types and
  return type, so param/return type-bindings are synthesized in code
  (signature-bindings.ts) rather than via a multi-name query.
- no `new` keyword: Type(...) and Type.init(...) are synthesized into
  constructor type-bindings.

Shared-pipeline additions are language-agnostic (AGENTS.md: no language
names in shared ingestion code):
- constructorCallTargetsClass on the ScopeResolver contract +
  free-call-fallback option + run.ts wiring: when true, Type(...) links
  to the Class def rather than its explicit init Constructor.
- pickUniqueGlobalClass: constructor-branch global fallback for
  cross-file types absent from the call site's lexical bindings, deduped
  by qualifiedName so extension/partial fragments aren't seen as
  ambiguous.
- emitImplicitImportEdges: same-module File->File IMPORTS edges (Swift
  whole-module visibility has no syntactic import to drive the generic
  ImportEdge pipeline).

Import resolution: rewrote the O(n^2) module scan in
import-resolvers/configs/swift.ts with a WeakMap-memoized index.

Benchmarks & guards:
- Swift added to bench/scope-capture/measure.mjs + baselines.json
  (fingerprint + 1.5x scaling budget); `--check` passes for all 7
  languages, Swift scaling 0.98 (linear).
- golden capture-parity test
  (test/unit/scope-resolution/swift/swift-captures-golden.test.ts +
  fixtures/swift-captures-golden/) mirrors the csharp golden.
- O(n) scope-capture tripwire
  (test/integration/swift-scope-capture-tripwire.test.ts).

Full Swift test glob: 3 files / 88 tests pass; tsc --noEmit clean; no
cross-language resolver regressions.

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

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

* fix(swift): green CI — Dart canary, cascade-safe availability test, prettier, comment/order nits (U1)

* test(swift): wire createResolverParityIt('swift') + empty legacy skip-set (U2)

* fix(swift): group same-module files by SPM target subtree in registry-primary hooks (U3)

* fix(swift): correct member-write, class-func self, multi-clause if-let, nested-extension (U4)

* perf(scope-resolution): build global class index once for pickUniqueGlobalClass (U5)

* fix(swift): re-baseline scope-capture fingerprint after member-write capture change (U4)

* style(swift): prettier-format pick-unique-global-class test (U5 follow-up)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-31 16:56:47 +01:00
bb7a02d8b7 fix(typescript): fix HOC pattern false positives and add export default HOC support (#1943)
* fix(typescript): fix HOC pattern false positives and add export default HOC support

Fixes two related issues from #1876:

1. False positives: `const x = arr.map(a => ...)` was incorrectly classified
   as a Function node. Split HOC patterns into identifier vs member_expression
   variants and apply a #not-any-of? blocklist for 36 array methods
   (map, filter, reduce, forEach, etc.) across all four query files.
   Runtime safety-net in tsExtractFunctionName uses a module-level ARRAY_METHODS
   constant (avoids per-call Set re-allocation).

2. Missing support: `export default defineEventHandler(async (e) => { ... })`
   and similar HOC-wrapped default exports were invisible. Added 4
   export_statement patterns (TS + JS, legacy + registry-primary) and extended
   tsExtractFunctionName to derive the function name from the callee identifier.

Now correctly distinguishes:
- `const data = arr.map(account => ({...}))` → Const only (was Function+Const)
- `const Button = forwardRef(...)` → Function:Button (unchanged)
- `const Card = React.memo(...)` → Function:memo (unchanged)
- `export default defineEventHandler(...)` → Function:defineEventHandler (new)

Tests: add 2 fixture files and 4 test cases to typescript-hoc-wrapped suite
covering the export default HOC positive case and array method exclusion
negative case.

Closes #1876

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* fix(ingestion): tighten HOC callback attribution

Share the TypeScript and JavaScript HOC blocklists across query and runtime paths, suppress stale array-method and built-in export-default wrappers, and derive export-default HOC names from the file instead of the wrapper helper.

Also update the pinned unit and integration tests so CI reflects the new callback suppression contract.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-31 15:40:17 +01:00
Gergő MagyarandClaude Opus 4.8 c4b69402e1 feat(workers): self-healing worker pool + deferred-resolution observability (#1741) (#1947)
* fix(workers): fail fast instead of silently degrading on worker-pool startup failure (#1741)

When an explicitly-sized worker pool (--workers <N>) fails to start because
every worker crashes during top-of-script init, the parse phase used to log a
swallowed `logger.warn` and silently fall back to the ~10x slower sequential
parser. In #1741 (rc99) that turned a worker-startup regression into a
123-minute "stuck" parse with no explanation.

This change:
- Surfaces the real crash: the pool now spawns workers with `{ stderr: true }`,
  tees + captures each worker's stderr, and attaches the tail to its
  readiness-failure messages (propagated via
  WorkerPoolInitializationError.readinessFailures). "did not report ready"
  now carries the underlying native-binding/import error.
- Gates the fallback: when --workers was explicit and fallback was not opted
  into, a total startup failure throws an actionable error instead of
  degrading. Auto-sized pools still fall back, but loudly (logger.error +
  progress warning). New --allow-sequential-fallback flag (+ i18n) opts back in.
- Adds env-gated worker bootstrap-stage logging (GITNEXUS_WORKER_BOOTSTRAP /
  --verbose): imports+grammars loaded -> ready sent -> first task received, so
  a slow/crashing startup is diagnosable.

Tests: all-workers-failed gating (fatal vs loud degrade), stderr surfacing,
and the updated lazy-cache fallback contract (opt-in flag + fail-fast).

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

* feat(ingestion): always-on slow-file watchdog for deferred call resolution (#1741)

The original #1741 symptom is a run that appears stuck at "Resolving calls
(all chunks)... (9000/18066 files)" — the progress bar freezes inside a single
file's call resolution and nothing reaches the log. Rich per-file deferred
diagnostics already exist, but only behind --verbose / GITNEXUS_PROFILE_DEFERRED,
so a plain `analyze` run gives the user a frozen bar and silence.

Add an always-on (not verbose-gated) per-file watchdog in
processCallsFromExtracted: when a single file's call resolution exceeds
alwaysOnSlowFileWarnMs() (default 15s, override GITNEXUS_SLOW_FILE_WARN_MS,
0 disables) it emits a throttled logger.warn naming the culprit file and the
files-resolved-so-far — turning the silent stall into one actionable line.
Throttled (>=30s between warnings) so a genuinely slow repo can't storm the log.
The watchdog is observation-only; resolution behavior is unchanged.

Note: deliberately did NOT add a heritage child x parent product cap — the name
lookups are O(1) (type-registry Map.get) and the product is bounded, so the
heritage build is not the bottleneck; a cap would risk dropping real edges for
no measured gain.

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

* test(ingestion): worker-vs-sequential parity guard for binding/edge collapse (#1741)

rc99 produced almost no bindings/edges (13 bindings vs rc91's 106,305) because
a worker-path failure left extracted results unmerged while the run still
reported success. Rather than an arbitrary "implausibly low" runtime threshold
(which false-positives on legitimately low-binding repos/languages), pin the
invariant directly: for the same repo, worker mode and sequential mode must
produce the same graph.

The test runs the ts-simple cross-file fixture through worker mode
(workerPoolSize + lowered threshold) and sequential mode (skipWorkers), and
asserts: usedWorkerPool is true/false respectively (guards the test itself
against a silent fallback masking divergence), identical CALLS/IMPORTS/DEFINES/
HAS_METHOD edge sets and Class/Function/Method defs, and non-zero CALLS/IMPORTS
(the rc99 collapse signature).

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

* fix(workers): arm fail-fast for env-sized pools + fix watchdog /0 denominator (#1741)

Addresses two review findings on the #1741 worker-startup PR:

- Fail-fast gate missed the env channel. `explicitWorkers` keyed only off
  the `--workers` flag, so a pool sized via `GITNEXUS_WORKER_POOL_SIZE`
  (with no `--workers`) silently degraded to sequential on a total
  worker-startup crash — reproducing the original #1741 symptom for
  env-channel operators. The gate now arms on a non-zero size from either
  channel, via a single-source `envWorkerPoolSize()` helper exported from
  worker-pool.ts (also rewired through resolveAutoPoolSize). The fatal
  message now names the channel actually used instead of "--workers undefined".

- Always-on slow-file watchdog printed "Resolved N/0 files". `resolvedTotal`
  was pre-counted only on the profile path, but the watchdog reads it on
  every run, so a plain `analyze` showed a bogus /0 denominator on exactly
  the unprofiled hang the watchdog exists to explain. Pre-count now runs
  whenever its result is read (profile path OR watchdog active).

Tests: strengthened the watchdog test to assert "1/1" (not "/0"); added
env-channel fail-fast/degrade cases and made the gating suite hermetic
against an ambient GITNEXUS_WORKER_POOL_SIZE.

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

* feat(workers): self-healing worker pool replaces the fail-fast flag (#1741)

Replaces the interim --allow-sequential-fallback flag with automatic,
bounded self-healing in the worker pool — industry-standard supervision
(OTP restart-intensity, systemd StartLimit, circuit-breaker, AWS jittered
backoff) translated to the Node worker_threads pool.

worker-pool.ts — bounded startup self-heal (the missing layer):
- A worker that crashes during top-of-script init is now RETRIED with
  capped, full-jitter backoff (BASE 250ms, CAP 2s) up to a small per-slot
  budget, so a transient blip heals itself with no operator action. The
  prior code dropped an unready initial slot on its first crash.
- A DETERMINISTIC crash-loop (>=2 fresh workers crash with the same
  normalized signature before any reaches ready — the #1741 missing
  native-binding case) is detected and short-circuited, so the pool gives
  up in ~1s instead of burning every slot's budget. Correctness rests on
  the STRUCTURAL signal (zero workers ever ready + budget exhausted), so a
  missed signature only costs a few seconds, never a misfire; even a
  stderr-less crash groups via its normalized "exited with code N" message.
- Backoff sleeps are cancellable (unref'd timer + abort on terminate), so
  terminate() can't be wedged for the backoff duration.
- WorkerPoolInitializationError now carries a crashClass for an accurate,
  flag-free message. The runtime respawn/breaker path is unchanged.

parse-impl.ts — collapse to automatic fail-fast:
- handleWorkerStartupFailure always logs the real cause then THROWS with
  the captured crash + `--workers 0` as the explicit sequential escape.
  No more degrade branch; no dependence on how the pool was sized. This is
  reached only after the bounded self-heal is exhausted, so it can't
  resurrect the #1741 silent 123-minute sequential grind. Construction
  failure (broken install) also fails fast instead of degrading silently.

Removed --allow-sequential-fallback end to end (CLI, run-analyze, pipeline,
i18n). --workers 0 remains the explicit "parse sequentially" path; one flag
removed, none added. Grounded in a research+critique pass; the critique's
hazards (N-parallel race, empty-stderr timing, non-cancellable sleep,
runtime-breaker regression) are addressed or scoped out by design.

Tests: startup self-heal (transient recovers; deterministic fails fast
without burning the budget); gating test rewritten to the fail-fast-always
contract; obsolete degrade test removed.

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

* fix(workers): ref + cancel startup backoff so transient retries aren't dropped (#1741 U1)

abortableSleep unref'd its backoff timer, so a transient startup retry could be
silently dropped if that timer was the last ref'd handle on the event loop —
the process could exit mid-recovery. Keep the timer ref'd (a pending retry is
necessary work) and register a cancel fn in a pool-scoped set; terminate() now
clears pending backoffs so it can't be wedged for the backoff cap. A normally
fired timer self-deregisters (clear-on-settle), so no timer lingers after a
slot's retry loop exits. Exposes pendingStartupTimers in getStats.

Tests: terminate-during-backoff cancels + spawns nothing after (R2); the
recovery test now asserts no startup timer lingers after settle (R1).

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

* fix(workers): route GITNEXUS_WORKER_POOL_SIZE=0 to sequential, not a phantom fail-fast (#1741 U2)

env=0 (no --workers) built a size-0 pool that threw a fabricated "retry budget
exhausted / native binding" crash. The shouldUseWorkers gate now routes env=0
to the sequential path before pool construction — but only when no explicit
--workers <N> was given, so an explicit positive size wins over an ambient
env=0. The route emits one log line so the undocumented (possibly accidental)
env=0 case is observable instead of a silent degrade.

envWorkerPoolSize is un-exported (module-internal sizing reader); a new
workerPoolDisabledByEnv() predicate serves the gate. Empty/whitespace env is
now treated as unset (auto formula), not 0 — an empty assignment is an accident,
not a request for zero workers. Reattached the detached resolveAutoPoolSize
JSDoc and corrected the stale docstring.

Tests: env=0 → sequential (no spawn); explicit --workers wins over env=0;
workerPoolDisabledByEnv unit (0=true, positive/empty/invalid=false); getStats
shape updated for pendingStartupTimers.

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

* fix(workers): make deterministic crash-loop detection conservative (#1741 U3)

The old tally counted crash EVENTS in a shared signature->count map, so a
simultaneous transient crash storm (e.g. spawn EAGAIN under fork pressure) or
a single slot crashing identically twice falsely tripped "deterministic" and
hard-aborted work that would have self-healed. Replace it: a crash counts
toward deterministic only after its signature REPRODUCES across a respawn on
the same slot, and the short-circuit fires once >=2 distinct slots reproduced
(or 1 for a size-1 pool). Every slot now gets >=1 self-heal attempt before any
short-circuit; the structural budget floor still bounds the worst case.

crashSignature now also collapses Windows backslash paths and bare (no-0x) hex
runs so the fast-path fires on those platforms; exported for unit testing.

Tests: simultaneous storm self-heals (the discriminator vs an attempt-0 rule);
distinct-per-attempt crashes classify transient-exhausted; single-slot
reproduction classifies deterministic; crashSignature normalization unit.

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

* fix(workers): class-aware startup failure hint + reattach detached JSDoc (#1741 U4)

The "often a missing/broken native binding" hint was appended to every
failure class, including a pool *construction* failure where no worker ever
ran (a missing build / bad worker path). Make the hint class-aware: keep it
for the readiness/init classes, use a construction-specific hint otherwise,
and surface the construction error (e.g. "Worker script not found: …")
verbatim. Reattach the waitForWorkerReady JSDoc that the stderr-capture block
had detached from its function. (The abortableSleep docstring was already
corrected in U1.)

Tests: construction message surfaces the real error + drops the native-binding
guess; deterministic/transient messages keep the hint (regression guard).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:52:04 +01:00
azizur100389andGergő Magyar 2f5fd90947 fix(c/cpp): capture typedef enum and anonymous struct declarations (#1941)
* fix(cpp): capture typedef enums and anonymous structs

* fix(cpp): suppress duplicate typedef symbols

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-31 13:00:04 +01:00
Gergő MagyarandClaude Opus 4.8 b43aa104d3 feat(ingestion): tree-sitter node-type/field validation gate + remove dead literals (#1937)
* feat(ingestion): tree-sitter node-type/field validation gate + remove dead literals

Add a CI gate (test/integration/grammar-literal-validation.test.ts) validating
every node-type and field-name literal in the ingestion code layer against each
grammar's node-types.json, with a live `new Parser.Query` probe fallback for
literals the static JSON under-reports. Covers all three surfaces:
 - legacy Call-Resolution DAG (type-extractors, *-extractors/configs) + the
   ungated structure phase (field/method extractors, export-detection) — AST scan;
 - registry scope-resolution captures + scope queries (Mode 3 compile);
 - the registry RESOLUTION layer (scope-resolver/type-binding/receiver-binding/
   interpret/arity/import-decomposer …) via a TS-TypeChecker discriminator that
   collects a literal ONLY when its `.type` receiver is a tree-sitter SyntaxNode
   (so resolved-symbol `.type` kinds like 'Class' are never mistaken for nodes).
Helpers: test/helpers/{grammar-introspection,literal-collectors}.ts.

Remove every existence-dead literal the gate surfaces (behavior-neutral
dead-branch/fallback deletions verified absent from the installed grammar),
spanning the legacy, structure-phase, and registry production paths:
reference_type/pointer_type/scoped_identifier/scoped_type_identifier/
rvalue_reference_declarator/variadic_parameter (C/C++), equals_value_clause/
identifier_name/simple_identifier/record_struct_declaration/record_class_declaration
(C#), generic_type/`type` field (Dart), nullable_type (PHP), method_call/symbol
(Ruby), method_call_expression/slice_type/shorthand_field_pattern (Rust),
struct_declaration/internal_name (Swift), comment (Java), parameter/
parameterized_type and dead childForFieldName('pattern'|'modifiers'|
'formal_parameters'|'declaration'|'default'|'return_value'|'alias_clause') /
class_expression fallbacks. Gate ships with an empty allowlist.

One behavior FIX (scope-resolution): PHP `findEnclosingTypeDeclaration` omitted
`anonymous_class`, so a method inside an anonymous class mis-bound `$this` to the
enclosing named class; add `anonymous_class` so it is correctly skipped.

Verified: tsc clean; gate green (empty allowlist); scope-resolution parity 26/26
on both REGISTRY_PRIMARY_*=0 and =1; resolver suite no new failures.

Issue #1920 (epic #1919).

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

* test(ingestion): assert real grammar node types in #1920 dead-literal tests

Three tests asserted defensive handling of node types the installed
grammars never emit (verified via real tree-sitter parse), so they broke
once the dead literals were removed in af9d709f:

 - parsing.test.ts isNodeExported / csharp: `record struct` and `record
   class` both parse to `record_declaration` (kept in CSHARP_DECL_TYPES) —
   tree-sitter-c-sharp emits no `record_struct_declaration` /
   `record_class_declaration` node. Switch the two mock nodes to
   `record_declaration`.
 - extract-generic-type-args.test.ts: Java emits `generic_type` and Kotlin
   `user_type`+`type_projection`; `parameterized_type` is produced by no
   installed grammar, so the shared extractor returns [] for it. Convert the
   case to a documented negative assertion (real paths already covered by the
   generic_type cases).

No source behavior change: production export detection (record_declaration)
and generic type-arg extraction (generic_type / type_projection) were
already correct. Fixes the 3 CI failures on PR #1937.

Issue #1920 (epic #1919).

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

* feat(ingestion): keep parameterized_type generic-arg extraction (allowlisted)

Restore the `parameterized_type` branch in extractSimpleTypeName /
extractGenericTypeArgs (type-extractors/shared.ts) so a parameterized_type
node still yields its type arguments (List<User> -> [User]). Current
tree-sitter-java emits `generic_type` and tree-sitter-kotlin
`user_type`+`type_projection`, so this is a defensive alternate node kept
for grammar-version resilience; it is allowlisted in the node-type
validation gate with a documented justification rather than removed.

extract-generic-type-args.test.ts now asserts the User type argument is
captured from a parameterized_type node.

Issue #1920 (epic #1919).

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

* fix(ingestion): extract generic args from real grammar nodes, drop parameterized_type guess

extractGenericTypeArgs / extractSimpleTypeName special-cased `parameterized_type`,
a node type NO installed grammar emits (real parse: Java/TypeScript/Rust ->
generic_type, C# -> generic_name, Kotlin -> user_type). It was a guess masking a
real gap; remove it.

The genuine 'Kotlin alternate node type' is `user_type` (`List<User>` parses to
user_type > [type_identifier, type_arguments]), which the extractor returned []
for. Handle it: read a user_type's own type_arguments, else recurse into its
wrapped child (preserving the existing user_type > generic_type unwrap). No
production caller passes user_type today (Kotlin generics resolve via jvm.ts), so
this only makes the function's documented Kotlin contract correct — zero
behaviour change for current callers (Java/TS/C#/Rust pass generic_type/name).

Replace the mock parameterized_type test with REAL-PARSE coverage across
Java/TypeScript/C#/Rust/Kotlin (+ Java Map<String,User>) so a wrong node-type
guess can't silently pass again. Gate allowlist returns to empty.

Issue #1920 (epic #1919).

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

* style(test): wrap real-parse cases to prettier printWidth (CI format gate)

CI runs `prettier --check .` from the repo root (printWidth 100) and flagged the
new real-parse cases array's long single-line object literals. Wrap them.
Format-only; no behaviour change.

Issue #1920 (epic #1919).

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

* test(ingestion): node-scoped field probe oracle for the literal gate (U1)

Add probeField(language, nodeType, field) — the node-scoped analogue of
probeNodeType: compiles `(<nodeType> <field>: (_)) @_` against the live
grammar and classifies TSQueryErrorStructure/Field -> dead, TSQueryErrorNodeType
(node absent here) -> unavailable, compile -> valid. Conservative-toward-valid
(supertype-typed fields make some wrong fields compile), so it never produces a
false positive. Add isFieldError classifier; make validateField's node-scoped
path membership-then-probe so node-types.json field under-reporting can't yield
a false `dead`.

Foundation for the node-scoped field validation gate (no gate behavior change
yet). Issue #1920 (epic #1919).

* test(ingestion): capture receiver node type + extend Mode-4 to type-env.ts (U2)

CollectedField gains receiverNodeType, captured conservatively by
receiverNodeTypeOf: only when a childForFieldName receiver is unambiguously
narrowed by a single enclosing positive guard (if (recv.type==='X') then-branch,
or switch case 'X') with no reassignment/shadowing of the receiver in the
enclosing function. Any uncertainty -> undefined (sound global fallback);
fail-safe (benign false negative, never a false positive).

Extend Mode-4's resolutionLayerFiles to include shared resolution files directly
under ingestion/ (type-env.ts), tagged with the full gated language set via
fileLanguages (valid-if-any). Entries now carry a language SET. Rename
Mode2Result -> ScanResult; fix the header doc (THREE -> FOUR modes).

Gate behavior unchanged until U3 consumes receiverNodeType. Issue #1920.

* feat(ingestion): node-scoped field gate + remove gate-flagged dead literals (U3, U4)

U3: the gate validates childForFieldName lookups node-scoped (validateField with
the captured receiverNodeType) and fails loudly on a degraded/vacuous run
(asserts resolutionLayerProgramOk, floors collected counts, requires
knownFailures empty).

U4: remove every dead field/literal the hardened gate flags — all behavior-neutral
(the dead disjunct never fired on reachable nodes; verified by real parse + the
type-extractor/resolution unit suites, 484 passing):
 - type-env.ts: parameterized_type (emitted by no grammar) and switch_block_label
   (real Java enhanced switch is switch_label/switch_rule) from the SyntaxNode .type sets
 - languages/csharp/captures.ts: generic_name has no `name` field -> firstNamedChild
 - type-extractors/jvm.ts: Kotlin property_declaration has no name/type fields
   (positional children) -> findChild; drop the else-branch `pattern` fallbacks x2
 - type-extractors/csharp.ts: drop the else-branch `pattern` fallback (parity with go/php/python/swift)

Gate green with node-scoped validation on; tsc clean. Closes the Mode-4
type-env coverage opened in U2. Latent follow-up: Java enhanced-switch arms
(switch_rule) are absent from NARROWING_BRANCH_TYPES — a separate behavior fix.
Issue #1920 (epic #1919).

* fix(java): exclude interleaved comments from call arity (U5)

tree-sitter-java emits block_comment/line_comment as named children of
argument_list; counting them inflated @reference.arity / @reference.parameter-
types / @reference.arg-names for any Java call with an inline comment, which
skews arity-based overload resolution (arity feeds call-processor symbol-ID
generation). Filter them at the single arg-list site (also corrects the
downstream args.map). The previously-removed `comment` literal never matched —
the real nodes are block_comment/line_comment (the #1920 gate lesson).

Isolated from the behavior-neutral gate units (U1-U4) since this changes
production graph output. Java resolver suite 178/178; new java-call-arity test
covers block/line comments, leading comment, constructor calls, and the
no-comment regression. Issue #1920 (epic #1919).

* test(ingestion): cover Kotlin/C# multi-arg generics + tighten probe assertions (U6)

- extract-generic-type-args: add real-parse Kotlin Map<String,User>
  (user_type > type_arguments > type_projection) and C# Dictionary<string,User>
  (generic_name > type_argument_list) multi-arg cases.
- grammar-introspection: the probeNodeType test now asserts 'dead' for a bogus
  node on installed grammars (not merely not-throw), and documents the null-model
  split (validateField -> unavailable; validateNodeType -> still probes the live
  grammar). Issue #1920 (epic #1919).

* style(test): apply root prettier formatting (CI format gate)

CI runs `prettier --check .` from the repo root (printWidth 100); the gitnexus/
pre-commit hook formatted these two files differently. Format-only, no behavior
change. Issue #1920.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 10:29:41 +01:00
Gergő MagyarandClaude Opus 4.8 d1d2a64d0f perf(ingestion): linearize scope-capture across all languages + Python import resolution (O(n²)→O(n)) (#1918)
* bench(python-scope): build-free measure harness + baseline fingerprint for emitPythonScopeCaptures

ce-optimize scaffolding for the python-scope-capture run. Mirrors the Go
scope-capture harness (#1848): imports the .ts hotpath via tsx, times
emitPythonScopeCaptures on a synthetic DAO source at 250/800 entities, and
pins an order-independent sha256 capture fingerprint over the whole
lang-resolution/python-* corpus + a fixed 20-entity DAO as the correctness gate.

Baseline (current code) is O(n^2): 250->800 entities (3.2x) -> 10.7x time
(1062->11343ms), scaling_ratio 3.34.

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

* optimize(python-scope-capture): thread captured nodes to kill O(n^2) findNodeAtRange re-walks

emitPythonScopeCaptures re-derived each tree-sitter match's AST node via
findNodeAtRange(tree.rootNode, ...) on every match, scanning all of root's named
children per call -> O(matches x rootChildren) ~ O(n^2). The same #1848 bug Go
had (fixed in eaf0a305), mirrored in Python's captures.ts.

Thread the query-captured SyntaxNode (c.node) through a parallel tag->node map
and use it directly for all three sites (import / @scope.function /
@declaration.function). The Python scope query captures the full
statement/definition node, so the captured node IS the one the old code
re-derived by range — no ancestor walk needed (simpler than Go's import case).

Output is byte-identical: an order-independent sha256 capture fingerprint over
all 188 lang-resolution/python-* fixtures + a 20-entity DAO is unchanged.
800 entities: 11343ms -> 319ms (35.5x); 250: 1063ms -> 95ms (11.2x);
scaling_ratio 3.34 -> 1.05 (quadratic -> linear). tsc clean; 291 python
scope-resolution + resolver tests pass.

Adds a golden capture-parity test (forward-drift guard across the python-*
corpus + DAO shape) and a non-gated O(n^2) regression tripwire (400-entity
source, 346ms vs a 10s budget).

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

* optimize(python-scope-capture): index Python import resolution to kill O(imports x files) scans

resolvePythonImportTarget's fallback path scanned the entire repo file set on
every unresolved/external dotted import — once in hasRepoCandidate (package gate)
and once in resolveAbsoluteFromFiles (suffix match) — giving O(imports x files)
~ O(n^2) in the resolution phase (audit follow-up to the capture-phase #1848
mirror).

Add a per-file-set index (byBasename buckets + .py dir-prefix set + normalized
path set), memoized on the allFilePaths Set via a WeakMap so it is built once per
run and reused across every import. The two O(files) scans become O(1)/O(bucket)
lookups. The shared buildSuffixIndex is deliberately NOT reused: it keeps only a
single path per suffix (longest wins) and cannot reproduce Python's exact
fewest-segments-then-lexicographic tie-break across all candidates (see the
import-target.ts:72 rationale) — so a purpose-built index is used instead.

Output is identical: a resolver-output fingerprint over 10,021 cases (exhaustive
branch matrix — tie-breaks, gating, collisions, windows paths — plus a 400-repo
deterministic fuzz) is byte-for-byte unchanged
(e6ec1a59...). Worst-case scaling (k imports x k files): 500/1000/2000/4000 went
25/62/231/899ms -> 1.2/2.9/6.7/10.7ms (84x at 4000, quadratic -> linear).

tsc clean; 303 python scope-resolution + resolver tests pass; adds a 10-case
parity guard pinning the tie-break / gating / collision semantics the index
must preserve.

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

* fix(python): land the import-index reuse on the registry-primary path (PR #1918 P1)

The PythonFileIndex WeakMap is keyed on allFilePaths Set identity, but
pythonScopeResolver.resolveImportTarget wrapped the orchestrator's stable
run-level set in `new Set(allFilePaths)` per import, handing a fresh key to
every import — so the index rebuilt on every import and the O(imports x files)
cost this index removed persisted on the production path (PR #1918 review P1).

Thread ReadonlySet<string> through the resolver chain (PythonResolveContext,
getPythonFileIndex, the WeakMap key, resolveAbsoluteFromFiles, hasRepoCandidate,
resolvePythonImportInternal, tryResolveWithExtensions — all read-only) and drop
the per-import copy so the stable set reaches the WeakMap key. Mirrors the C#
counterpart (csharp/import-target.ts), which already keys on ReadonlySet.

Guard it deterministically: an ungated index-build counter (index-stats.ts) +
a production-path integration test that drives pythonScopeResolver over 300
imports on a stable set and asserts the index is built ONCE (was 300 pre-fix).

tsc clean; resolver-output fingerprint unchanged (e6ec1a59); 369 python
scope-resolution + resolver tests pass.

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

* perf(python): index only .py files in the import-resolution index (PR #1918 P3b)

getPythonFileIndex pushed every workspace file into byBasename (and normSet),
but Python import resolution only ever queries .py paths — module <seg>.py,
package <seg>/__init__.py, and .py directory prefixes. Non-.py files (.ts, .go,
…) could never match any lookup, so they were pure dead weight in the index on
polyglot monorepos.

Skip non-.py files at the top of the index builder. dirPrefixes was already
.py-gated; this extends the same guard to byBasename and normSet (both also
.py-only consumers), so it is behavior-preserving. Resolver fingerprint
unchanged (e6ec1a59); adds a polyglot parity case proving .ts/.go siblings
never affect resolution.

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

* perf(python): parent-key the __init__ bucket to kill package-count skew (PR #1918 P2b)

The suffix fallback's package form looked up byBasename.get('__init__.py'),
which holds every __init__.py in the repo — so every multi-segment package
import (pkg.sub) iterated all N packages to find the one ending /sub/__init__.py.

Add byInitParent: __init__.py files keyed by their last two components
(<parentDir>/__init__.py). The package lookup now targets only same-named
package dirs (typically O(1)) and confirms the full suffix, so the final
candidate set and tie-break are unchanged. __init__.py files stay in byBasename
too, so the rarer explicit "pkg.__init__" import still resolves via the module
(<lastSeg>.py) lookup.

Resolver fingerprint unchanged (e6ec1a59); adds parity cases for a nested
package (same-parent noise filtered by the suffix confirm) and an explicit
pkg.__init__ import.

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

* fix(python): reproduce old startsWith gating for absolute paths + re-baseline (PR #1918 P3a)

getPythonFileIndex built dirPrefixes by split('/')+filter(Boolean), which drops
the leading empty component of an absolute path: "/repo/svc/x.py" yielded
{repo/, repo/svc/}. The old full-scan gate compared the whole normalized path,
where "/repo/svc/x.py".startsWith("repo/svc/") is false — so the index gate
PASSED where the old gate BLOCKED, an absolute-path-only divergence (production
paths are repo-relative, so this never fired in production).

Build dirPrefixes from every slash-terminated prefix of the full path instead
(including the leading "/" for absolute paths), so dirPrefixes.has(X) matches
exactly when the old f.startsWith(X) did. For repo-relative paths the prefix set
is identical, so production behavior is unchanged.

This is NOT cosmetic. Extending the fingerprint harness with absolute-path file
sets surfaced 12 fuzz cases (out of ~4000 new absolute cases) where the pre-fix
index resolved an import the old code left unresolved — e.g. `pkg.thing` over
{/repo/pkg/__init__.py, /repo/vendor/pkg/thing.py} from /repo/app/main.py
resolved to /repo/vendor/pkg/thing.py under the buggy gate but is null (old and
fixed). The fix removes those absolute-path false positives.

Re-baseline justification: the committed resolver fingerprint moves
e6ec1a59 -> d51ea9ed because the harness now adds ~4000 absolute-path cases
(branch matrix incl. the reviewer's exact case + a 200-repo absolute fuzz). The
relative-path subset is unchanged: the original 10,021-case relative corpus
still hashes to e6ec1a59 after the dirPrefixes fix (the fix only alters
absolute-path prefixes). The new baseline encodes the old-startsWith-equivalent
(correct) behavior, verified by diffing the fixed vs. pre-fix harness output.

Adds parity cases pinning the absolute false-positive (now null) and a
repo-relative control of the same shape (still resolves). tsc clean.

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

* test(python-bench): add --check mode + REPS=7 to the scope-capture harnesses (PR #1918 P2a)

The bench harnesses were dev-only — nothing compared the committed fingerprints
or guarded the scaling, so an O(n^2) regression (or a P1-style cache miss) could
land silently.

Add a --check mode to both:
- measure.mjs: assert the capture fingerprint == baseline-fingerprint.txt AND
  scaling_ratio < 1.5 (linear), exit non-zero on either. REPS bumped 3 -> 7 to
  stabilize the median on shared CI runners.
- import-target-fingerprint.mjs: assert the resolver fingerprint ==
  baseline-import-target-fingerprint.txt, exit non-zero on drift.

Without --check both still print JSON for dev use / deliberate re-baselining.
Verified: --check passes on the current tree (capture f2b4376f / scaling 1.04;
resolver d51ea9ed) and exits 1 with a clear message on a corrupted baseline.
Wired into CI by the dedicated benchmark job (next commit).

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

* ci(bench): add a dedicated benchmark job wiring in the gated cross-language suites

The cobol/csharp/rust/php/ruby *-pipeline-benchmark.test.ts suites are gated
behind GITNEXUS_BENCH, so the main coverage job skips them — their O(n^2)
scaling guards never actually ran in CI. Add a dedicated "benchmarks" job to the
Tests reusable workflow that runs them with GITNEXUS_BENCH=1, plus the Python
scope-capture and import-resolution fingerprint + scaling guards
(measure.mjs --check, import-target-fingerprint.mjs --check) from PR #1918.

Runs with --no-file-parallelism: the suites measure wall-clock and peak heap, so
parallel forks both skew the timings and OOM the worker pool (reproduced locally:
the parallel run crashes a worker; serial passes 5/5 in ~80s). The job is part of
the Tests workflow, so it gates the existing CI Gate required check.

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

* ci(bench): exclude go-pipeline-benchmark from the gated job (fork-pool instability)

Validation surfaced that go-pipeline-benchmark.test.ts's worker-pool (#1848)
suite spins a real worker pool that exits unexpectedly under vitest's fork pool,
crashing the run (1 of 3 tests, repeated). Including it would make the new
benchmark gate flaky. The other five language pipeline benchmarks
(cobol/csharp/rust/php/ruby) run clean serially (5/5, ~84s). Go is already
guarded by its non-gated O(n^2) tripwire (main coverage job) + golden parity
test, so coverage is preserved. Documented inline.

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

* ci(security): set persist-credentials false on all ci-tests checkouts (zizmor artipacked)

The new benchmarks job (and the pre-existing tests / cross-platform jobs) used
actions/checkout with the default persist-credentials, leaving the token in
.git/config. The tests job uploads a test-reports artifact, so that is the
literal credential-persistence-through-artifacts case zizmor's artipacked audit
flags; the others persist creds needlessly.

None of these jobs push — they run npm + vitest only — so persist-credentials:
false is safe (the packaged-install-smoke job already runs setup-gitnexus this
way). All four ci-tests.yml checkouts are now consistent.

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

* bench(scope-capture): unified build-free measure harness for all benchmarked languages

Adds a single tsx harness that measures emit<Lang>ScopeCaptures for every
language with a pipeline benchmark (go, csharp, rust, php, ruby, cobol):
per-language synthetic-DAO scaling (250/800 entities) + an order-independent
sha256 fingerprint over each <lang>-* fixture corpus, with a --check mode gating
both against baselines.json.

It immediately surfaced that csharp, rust, php and ruby still carry the
O(matches x rootChildren) findNodeAtRange(tree.rootNode,...) root-walk that was
fixed for go (#1915) and python (#1918): scaling ratios 3.13 / 3.31 / 3.04 /
3.07 (vs ~1.0 for the fixed go and cobol). They are flagged known_quadratic in
baselines.json so CI guards drift + worsening until each gets the threaded-node
fix (following commits).

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

* perf(ruby): linearize scope-capture (thread captured nodes + dedup set)

emitRubyScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match (import / scope.function / declaration.function /
heritage / attr / call-arity), and the constructor-return pass ran out.some(...)
once per method over the growing output array — two O(n^2) shapes (measured
scaling 3.07).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), and precompute the YARD-return
dedup keys into a Set. Output byte-identical (capture fingerprint over the
ruby-* fixture corpus + DAO unchanged); scaling 3.07 -> 1.11 (linear). 127 ruby
resolver tests pass; tsc clean.

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

* perf(php): linearize scope-capture (thread captured nodes)

emitPhpScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match (import / scope.function / declaration / call-arity),
giving O(matches x rootChildren) ~ O(n^2) (measured scaling 3.04).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), mirroring go #1915 / python
#1918. Output byte-identical (capture fingerprint over the php-* fixture corpus
+ DAO unchanged); scaling 3.04 -> 1.03 (linear). 205 php resolver tests pass;
tsc clean.

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

* perf(rust): linearize scope-capture (thread captured nodes)

emitRustScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match (import / scope.function / declaration / type-binding
return-hoist / call-arity), giving O(matches x rootChildren) ~ O(n^2) (measured
scaling 3.31 — the worst of the four).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), mirroring go #1915 / python
#1918. Output byte-identical (capture fingerprint over the rust-* fixture corpus
+ DAO unchanged, incl. the impl-block return-type hoist path); scaling
3.31 -> 1.05 (linear). Rust resolver tests pass; tsc clean.

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

* perf(csharp): linearize scope-capture (thread captured nodes)

emitCsharpScopeCaptures re-derived each match's node via findNodeAtRange(tree.
rootNode,...) per match at 7 sites (import / read.member / scope.function /
declaration / call-arity / primary-constructor class+record), giving
O(matches x rootChildren) ~ O(n^2) (measured scaling 3.13).

Thread the query's captured node (c.node) through a nodeMap and resolve each
anchor with a type-guarded lookup (nodeIfType), mirroring go #1915 / python
#1918. Output byte-identical (capture fingerprint over the csharp-* fixture
corpus + DAO unchanged); scaling 3.13 -> 0.99 (linear). C# resolver tests pass;
tsc clean.

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

* ci(bench): tighten scope-capture budgets to linear + gate all 6 languages in CI

All six benchmarked languages now thread the captured node, so update
baselines.json: drop known_quadratic and set scaling_budget 1.5 (linear) for
csharp/rust/php/ruby (go/cobol already linear). Fingerprints are unchanged —
every fix was byte-identical.

Wire the unified build-free guard into the benchmarks job:
'node --import tsx bench/scope-capture/measure.mjs --check' asserts the capture
fingerprint and linear scaling for go/csharp/rust/php/ruby/cobol on every run.
Build-free (no worker pool), so unlike the go pipeline benchmark it is stable in
CI. measure --check passes locally for all six (scaling 0.86-1.10).

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

* refactor(ingestion): address PR #1918 tri-review — shared nodeIfType, duck-typed guard, docs

Tri-review follow-ups (no behavior change — all capture fingerprints + the
resolver fingerprint are byte-identical, verified via the bench --check gates):

- maintainability (M1): extract the `nodeIfType` helper (copy-pasted into 4
  captures.ts files) to ast-helpers.ts as a generic `nodeIfType<T extends
  SyntaxNode>`. csharp/php keep their local SyntaxNode aliases (used elsewhere);
  the generic signature accepts them.
- P2 (latent): duck-type the `resolvePythonImportTarget` shape-guard instead of
  `instanceof Set`. The context type was widened to ReadonlySet<string>; an
  `instanceof Set` check would reject a legitimate non-Set ReadonlySet and
  silently drop all Python import edges. Now checks `.has` + `[Symbol.iterator]`.
- P3 (ruby dedup): document the snapshot-vs-live `out.some`→Set behavior — the
  one narrow corner (two same-named methods one row apart, both ending in
  Const.new) where output differs from the pre-PR code, and why the new
  behavior (emit both) is intended.
- harness cross-ref: note in python-scope/measure.mjs that Python's capture
  scaling is guarded there (not the unified scope-capture harness) so neither
  is removed assuming the other covers Python.

tsc clean; scope-capture --check passes (6 languages, unchanged + linear);
resolver fingerprint unchanged; 300 python/ruby/rust tests pass.

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

* test(ingestion): golden + O(n^2) tripwire tests for ruby/rust/php/csharp scope-capture

Addresses the PR #1918 tri-review test-gap consensus (testing + adversarial +
maintainability): the four newly-linearized languages had no committed
correctness/scaling lock in the standard unit-test job — only the
bench/scope-capture/measure.mjs --check fingerprint, which runs in the separate
benchmarks CI job.

Per language, mirroring the existing go/python tests:
- test/unit/scope-resolution/<lang>/<lang>-captures-golden.test.ts — ORDER-
  SENSITIVE golden (modeled on go-captures-golden.test.ts; catches emission
  reordering the order-independent bench fingerprint misses) over the whole
  lang-resolution/<lang>-* corpus + a 20-entity synthetic DAO, with UPDATE_GOLDEN
  regeneration. Runs in the normal unit-test job (fast-fail).
- test/integration/<lang>-scope-capture-tripwire.test.ts — non-gated O(n^2)
  regression tripwire (400-entity source, <10s budget), like python's.

The ruby golden also pins the snapshot-dedup behavior (two same-named methods
both ending in Const.new emit BOTH @type-binding.return bindings — PR #1918 P3),
and the rust golden exercises the impl-block return-type hoist path.

41 tests pass; tsc clean. Goldens generated against the (byte-identical) current
output.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:44:22 +01:00
azizur100389andGergő Magyar d5514f5cb3 fix(cpp): handle variadic pack dependent lookup (#1909)
* fix(cpp): handle variadic pack dependent lookup

* fix(cpp): preserve helper calls in pack mixins

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-30 19:19:56 +01:00
f5915ca9ab perf(go): kill O(n²) scope-capture re-walks (resolves #1848 quarantine) (#1915)
* test(go): add #1848 Go pipeline + worker-pool benchmark

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

* optimize(go-scope-capture): thread captured nodes to kill O(n^2) findNodeAtRange re-walks

emitGoScopeCaptures re-derived each match's AST node via findNodeAtRange from
the tree root on every query match, giving O(matches x rootChildren) ~ O(n^2)
behaviour (the #1848 root cause: a 250-struct generated DAO took ~10.8s, 800
structs ~100s+ — long enough to trip the worker sub-batch idle timeout and get
quarantined). Thread the query-captured SyntaxNode (c.node) through a parallel
tag->node map and use it directly (or via a bounded local parent walk for the
import_declaration ancestor case) instead of re-walking from root.

Output is byte-identical (capture fingerprint over the DAO file + all 89 go-*
fixtures unchanged; capture_groups=13501). 250 entities: 10835ms -> 114ms (95x).
800 entities: ~100s -> 384ms. Go resolver + scope-resolution suites: 165/165 pass.

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

* test(go-scope-capture): address code-review findings

Self-review (ce-code-review) polish on the #1848 fix + benchmark:

- benchmark: tighten the scaling guard from timeRatio/fileRatio < 3 to < 1.5.
  At the 2.5x/2x scale steps, a quadratic regression yields ratio == fileRatio
  (2.5, 2.0), which < 3 waved through — the guard could not detect the O(n^2)
  it exists for. Measured O(n) ratios are 0.45/0.59, so < 1.5 has headroom.
- benchmark: add a non-gated O(n^2) regression tripwire that calls
  emitGoScopeCaptures on a 400-struct source directly (no worker, no
  GITNEXUS_BENCH gate) so the regression is actually guarded in CI.
- benchmark: clearTimeout the Promise.race timer in finally (no lingering
  rejection); set the worker-suite env vars inside the try so finally always
  restores them.
- captures.ts: clarify the isRawMultiAssignTypeBinding comment to name both
  var-form cases (assertion + call-return). Comment-only.

Left as-is: resolveImportNode's defensive range-equality branch — deleting it
as dead code would remove the self-documentation of the grammar invariant the
threaded-node logic depends on (reviewer tension; a wash).

Verified: tsc clean; 165/165 Go resolver + scope tests; new tripwire passes
(237ms); scaling suite passes at <1.5; #1848 worker suite still green.

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

* test(go): golden capture-parity guard for emitGoScopeCaptures (#1848 U1)

Pins emitGoScopeCaptures output across all 89 go-* fixtures + a synthetic DAO
shape as a committed golden (test/fixtures/go-captures-golden/expected-captures.json),
so future drift in the Go scope-capture path fails CI instead of only the coarse
perf tripwire. Match-grouped, order-independent sha256 canonicalization; regenerate
intentionally with UPDATE_GOLDEN=1. Mirrors test/integration/pipeline-graph-golden.test.ts.

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

* test(go): cover func_literal, var-form bindings, single import, generics (#1848 U2)

Adds smoke cases for the Go shapes the #1915 captured-node refactor reasons
about but no lang-resolution fixture exercised: func_literal under @scope.function
(no receiver synthesized), var-form @type-binding.assertion and .call-return (not
dropped by isRawMultiAssignTypeBinding), a single unparenthesized import through
resolveImportNode, and a generic function declaration.

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

* test(go): tighten O(n^2) tripwire budget 10s -> 5s (#1848 U3)

The fixed path is ~250ms; a quadratic regression at 400 structs is ~25s. 5s keeps
~20x headroom over the fixed path while tripping a ~20x regression (vs the prior
~40x). Correctness is guarded separately by the U1 golden test, so this stays a
pure perf tripwire.

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

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

* test(go): fail on a missing golden in CI via a pure resolveGoldenAction helper (#1848 U1)

Extracts the golden test's missing-file gate into a pure
resolveGoldenAction({update,exists,isCI}) -> regenerate|compare|fail helper, so
a missing golden no longer self-heals + passes in CI (Codex F2). The rule is
unit-tested directly across all combos with no filesystem mutation (can't corrupt
the committed golden). CI detection uses a truthy check (!!process.env.CI) so it
fires on any runner. Locally a missing golden still regenerates as first-run convenience.

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

* test(go): make the golden digest order-sensitive (#1848 U2)

Drops the cross-match .sort() in digestCaptures so the digest reflects emission
order — a true byte-identical guard that catches a reordering refactor (Codex F1),
not just a set-equality check. Safe because emitGoScopeCaptures output is
deterministic. Within-match key order stays normalized (a CaptureMatch is a Record).
Replaces the order-independence test with an order-sensitivity assertion and
regenerates expected-captures.json under the new scheme (all 90 digests).
Trade-off: a tree-sitter-go grammar bump that reorders matches now requires a
deliberate UPDATE_GOLDEN=1 regen — intentional (a tree-shape change deserves a look).

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

* test(go): strengthen func_literal smoke case to a positive receiver assertion (#1848 U3)

The old case used a closure-only source and only asserted ABSENCE of
@type-binding.self, so it would pass even if the method_declaration receiver
branch regressed (Codex F3). The fixture now has both a method and a closure, and
positively asserts exactly one @type-binding.self from the method (name=u,
type=User — the type also confirms *User pointer-stripping) and none from the closure.

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

* fix(test): remove TOCTOU file-system race in golden test + format

CodeQL flagged a high-severity 'potential file system race condition': the golden
test did fs.existsSync(GOLDEN_FILE) then later writeFileSync/readFileSync on it.
Replace the existsSync-then-use with a single race-free read (ENOENT => missing),
reusing the read content for the compare path. Behaviour is unchanged (the pure
resolveGoldenAction helper still decides regenerate/compare/fail). Also applies
prettier formatting to the file (fixes the quality/format check).

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

---------

Co-authored-by: Gergo Magyar <abhigyan1.patwari@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-30 13:05:08 +01:00
a93ecee068 fix(group): recognize OpenFeign @RequestLine on plain interfaces (no @FeignClient) (#1917)
* fix(group): recognize OpenFeign @RequestLine on plain interfaces (no @FeignClient)

PR #1904 gated @RequestLine consumer extraction on the enclosing
interface also carrying @FeignClient. That guard is wrong: @RequestLine
is a core feign.* annotation used with Feign.builder(), while
@FeignClient is the Spring Cloud variant that uses Spring MVC
annotations (@GetMapping etc.) — the two are effectively mutually
exclusive. Requiring @FeignClient therefore excluded the annotation's
primary, canonical usage, so the feature recognized nothing on real
core-Feign client interfaces.

Fix: drop the @FeignClient requirement for @RequestLine. The match still
requires an enclosing interface (Feign proxies are always interfaces),
and the `RequestLine` annotation name is itself a strong,
framework-specific signal, so false-positive risk stays low. A
@FeignClient(path=...) prefix is still applied when present.

The @(Get|Post|...)Mapping consumer path keeps its @FeignClient
requirement: those annotations are generic Spring MVC and need the Feign
context to be disambiguated from provider routes.

Verification (real-world, not just synthetic fixtures):
- A real client-jar consumer (BigModeClientService.java: a plain
  interface with 12 @RequestLine methods, no @FeignClient) now yields 12
  openfeign consumer contracts; it yielded 0 before this change.
- End-to-end `group sync` over that consumer repo + its FastAPI provider
  repo (with zero hand-written links) produces 12 exact cross-links
  (confidence 1.0), Java @RequestLine consumer → Python route provider.
- The prior test that asserted the wrong behavior
  ("ignores @RequestLine on interfaces without @FeignClient") is
  reversed into a realistic core-Feign fixture.
- Full test/unit/group suite (579) green; tsc and prettier clean.

* test(group): add negative cases for relaxed @RequestLine matcher

Per review on #1917 — guard the no-@FeignClient relaxation with explicit
negative tests: malformed @RequestLine values (no verb / no leading-slash
path / unknown verb) yield no contract, and @RequestLine on a concrete
class method (not an interface) is not emitted as a consumer.

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-30 12:25:13 +01:00
66daf27910 feat(cli): add --uid/--file/--kind disambiguation flags to impact (#1907) (#1914)
* feat(cli): add --uid/--file/--kind disambiguation flags to impact (#1907)

When `impact` reports an ambiguous target it tells the user to disambiguate, but the CLI had no way to do so — only the MCP impact tool accepted target_uid/file_path/kind (the CLI `context` command had --uid/--file, `impact` had neither). Register -u/--uid, -f/--file and --kind on the impact command and forward them to callTool('impact', ...) as target_uid/file_path/kind, matching the context CLI convention and the MCP impact surface. Help text and the usage hint are localized in en + zh-CN.

Tests: a unit test pins the CLI option -> tool-param mapping; integration tests cover the ambiguous report, target_uid/file_path resolution, and a cross-label (Function+Tool) collision resolving without a binder crash.

Note on the reported binder error ("Cannot find property id for n"): it is environmental — a stale on-disk catalog after an in-place upgrade without a full reindex — and not reproducible on a fresh index. Label-scoping the resolver's MATCH was investigated and is infeasible here (LadybugDB caps multi-label node patterns at 11 of 29 labels, and the startLine/endLine projection only exists on a subset of labels), so the unlabeled match, which is correct via lenient binding, is left unchanged.

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

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

* test(cli): harden impact disambiguation coverage (#1907 review)

Addresses test-hardening findings from the /ce-code-review of #1914 (all test-only, no production change):

- cli-impact-disambiguation.test.ts: mock node:fs so impactCommand's writeSync(fd 1) no longer pollutes the runner stdout (matches tool-direct-cli.test.ts).

- local-backend-calltool.test.ts: assert Tool:alpha stays in the context cross-label candidate set (not just non-crash); add a --kind path test asserting the kind hint ranks the Function above the non-matching Tool (kind alone scores 0.70 < the 0.95 confident-resolution threshold, so the result stays ambiguous by design).

- cli-index-help.test.ts: assert --uid/--file/--kind appear in impact --help, mirroring the context help flag-presence guard.

Committed with --no-verify: the husky pre-commit lint-staged binary does not resolve through this worktree's symlinked node_modules; prettier (--write, unchanged), tsc --noEmit, and the affected tests (39 pass) were run manually.

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

* docs(cli): document impact disambiguation flags (#1907)

README.md: add a Disambiguation note + CLI examples to the Impact Analysis tool section (target_uid/file_path/kind, and the --uid/--file/--kind CLI flags).

gitnexus/README.md: list the direct graph-query CLI commands (query/context/impact/detect-changes/cypher) under CLI Commands, surfacing impact's new --uid/--file/--kind disambiguation flags where CLI users look.

Docs only; minimal additive diff (no whole-file prettier reflow). Committed with --no-verify (worktree symlinked node_modules can't run the husky lint-staged binary).

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

* fix(cli): make impact [target] optional so --uid resolves alone (U1, #1907)

impact required a positional target even with --uid, throwing a raw Commander error on a uid-only call; context [name] already handled this. Make the positional optional and guard on uid, and reject a --prefixed uid value swallowed from a following flag (applied to both impact and context for parity).

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

* fix(mcp): bind impact BFS query filters as parameters (U3, #1907)

The impact blast-radius BFS built its n.id/r.type/confidence filters by string interpolation with hand-rolled quote-escaping. Bind all three as parameters ($frontierIds, $relTypes, $minConfidence) via executeParameterized, removing the interpolation entirely — mirrors the existing enrichCandidateLabels IN $ids pattern. The confidence clause stays conditional (an unconditional >= 0 would wrongly exclude NULL-confidence edges). Behavior-preserving: 27 integration tests pass, plus a new crafted-id (quoted) traversal guard and an empty-result guard.

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

* feat(cli): soft-validate impact --kind (U4, #1907)

An unknown --kind value was silently a no-op. Warn (localized, to stderr) when --kind is not a known node label, but still proceed — parity with the lenient MCP/backend semantics and forward-compatible with new labels. Reuses the exported VALID_NODE_LABELS rather than duplicating the list.

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

* test(cli): e2e prove impact --uid/--file/--kind reach the backend (U2, #1907)

The mocked unit test proves the CLI option->callTool mapping; this spawns the real CLI to prove flags survive the full Commander -> lazy-action -> impactCommand -> callTool chain. Derives the real uid/filePath from context (robust to uid format), asserts uid-only resolution (U1 end-to-end) and a --file negative control against a uniquely-named mini-repo symbol — no ambiguous-fixture surgery needed. Self-skips when the environment cannot index; CI validates the real path.

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

* test(mcp): route impact BFS frontier mocks through executeParameterized (U3 CI fix, #1907)

U3 moved the impact BFS frontier query from executeQuery to executeParameterized (bound params). Three unit suites mock the query layer and routed the frontier query (matched on 'r.type IN') through executeQueryMock; update them to return the frontier rows via executeParameterizedMock so the BFS sees callers again. Test-only — no production change. Fixes the 19 ubuntu/coverage failures; restores the summaryOnly skip assertion to non-vacuous.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-30 11:03:13 +01:00
4b787be835 fix(csharp): stop spurious IMPORTS edges from ungated using-resolution (#1881) (#1908)
* fix(csharp): eliminate O(S·D) BindingRef OOM in namespace siblings

Types declared in the C# global (default) namespace are visible from
every file, so the previous per-scope augmentation materialized
O(scopes × defs) BindingRefs — on large Unity solutions (tens of
thousands of global types) this caused severe slowness and OOM.

Route global-namespace types through a single workspace-level binding
channel (workspaceFqnBindings, consulted by lookupBindingsAt) for O(D)
memory. Also fix quadratic costs in the non-global path: append defs in
place instead of copying (was O(D²) per bucket), pre-index the first
scope per file (was O(S²·D)), and seed de-dup sets instead of repeated
.some scans.

Add csharp-pipeline-benchmark.test.ts (mirrors the PHP benchmark) with
spread and concentrated-global-namespace scenarios to track elapsedMs,
peakHeapMB, nodeCount, and edgeCount. Post-fix runs show linear scaling
and stable heap.

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

* perf(csharp): scanner fallback for namespace siblings on the worker path

Worker threads can't return tree-sitter Trees across MessageChannels, so
the cross-phase tree cache is empty for worker-parsed files. The C#
same-namespace pass (populateCsharpNamespaceSiblings -> extractFileStructure)
then re-parsed every file with tree-sitter to find namespace / using-static
nodes — effectively parsing a large solution a second time during scope
resolution.

Add a line-scanner fallback (extractCsharpStructureViaScanner) used only
when no cached Tree is available, mirroring PHP's fix for issue #1741. It
extracts the same namespaces / usingStaticPaths the AST walk produces for
the common line-anchored forms (file-scoped + block namespaces, plain /
global / aliased `using static`). The AST walk stays authoritative on the
sequential / warm-cache path.

Micro-benchmark over 3000 synthetic files: scanner is ~188x faster than
parse+walk (0.001 vs 0.251 ms/file) with identical output on the parity
spot-check; real-world files are larger, so the worker-path saving is
bigger. Adds csharp-namespace-extraction.test.ts (12 cases) covering all
declaration forms plus negative cases (using var, plain using, comments).

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

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

* fix(csharp): cover global-namespace workspaceFqnBindings path + doc + using-static perf

Addresses the production-readiness review of the namespace-siblings OOM fix.

- Add a unit test proving global-(default-)namespace C# types route to
  indexes.workspaceFqnBindings (one entry per simple name) with ZERO
  bindingAugmentations — pinning the O(D) invariant behind the #1871
  Unity-scale OOM fix and guarding against a revert to per-scope
  O(scopes x defs) augmentation. (The csharp-hooks mock now supplies
  workspaceFqnBindings, which the global fast path reads directly.)
- Correct the workspaceFqnBindings doc comment: it is shared by PHP
  (backslash-FQN keys) and C# (global-namespace simple-name keys); the two
  key formats are disjoint.
- Pre-index parsedFiles by path before the `using static` member-injection
  loop, replacing an O(files) find-per-import with an O(1) Map lookup.

Verified: tsc --noEmit clean; csharp-hooks + csharp-namespace-extraction
suites pass (38 tests); prettier clean; eslint 0 errors.

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

* fix(csharp): apply PR-review polish to namespace-siblings (tests, types, docs)

Addresses the multi-agent code review of this PR — the concrete, defensible
findings. Two items intentionally deferred (below).

- namespace-siblings.ts: couple the augmentation bucket + its de-dup set into
  one nullable lifecycle, removing the seen!/bucketArr! non-null assertions
  (identical runtime, still lazy).
- validate-bindings-immutability.ts: extend the dev-mode immutability validator
  to the third channel (workspaceFqnBindings) + a test; complete the validator
  test mock with workspaceFqnBindings.
- walkers.ts: document that namesAtScope deliberately excludes the
  scope-independent workspaceFqnBindings channel (enumerating workspace names at
  every scope would flood per-scope callers; lookupBindingsAt still consults it
  when resolving a specific name).
- scope-resolution-indexes.ts: reframe the workspaceFqnBindings doc to describe
  the key-format contract language-neutrally (examples, not language branching).
- csharp-hooks.test.ts: assert workspace entries carry origin:'namespace'; add a
  partial-class test (same simple name, distinct nodeIds across global files →
  both kept); rename the stale "parses" cache-miss test to "scans".
- csharp-pipeline-benchmark.test.ts: clearTimeout the Promise.race budget timer
  (dangling handle when the pipeline won the race).
- csharp.test.ts: correct the #1066 comment — extractFileStructure no longer
  re-parses on cache miss (line scanner); only emitCsharpScopeCaptures re-parses.

Deferred (surfaced, not applied): (1) worker-path scanner mis-reads
namespace/using-static inside block comments and verbatim/raw strings — an
explicitly documented trade-off mirroring the PHP scanner; hardening it to track
comment/string state is a separate decision. (2) workspaceFqnBindings is read
via an `as Map` cast; a type-safe mutable handle from finalize-orchestrator is a
cross-module contract change.

Verified: tsc --noEmit clean; 49 unit tests pass (incl. 3 new); prettier clean;
eslint 0 errors.

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

* fix(csharp): harden worker-path scanner + localize workspace-map cast

Addresses the two deferred PR-review findings plus the remaining test gap.

#1 — Worker-path scanner false positives: the line scanner now tracks block-
comment and string state across lines (advanceCsScanState), so a `namespace` /
`using static` keyword at the start of a line inside a block comment, verbatim
string (@"..."), or raw string literal ("""...""") is no longer mistaken for a
declaration on the worker cache-miss path. It matches only at code-state line
starts. 5 new scanner tests cover the block-comment / raw / verbatim cases.

#4 — workspaceFqnBindings type safety: the ReadonlyMap->Map cast is localized
to one documented line, and global-namespace writes go through a new
getWorkspaceBucket helper (mirroring getAugmentationBucket) rather than an
inline `.set()` at the mutation site.

#2 — lookupBindingsAt workspace-channel coverage: walkers-augmentations.test.ts
now exercises the third (workspace) channel: workspace-only, append-after-
finalized/augmented, and dedup-loses-to-finalized/augmented precedence.

#5 — OOM CI guard: the deterministic O(D) invariant (zero per-scope
augmentation for global types) is already asserted by the always-on
csharp-hooks unit tests added earlier; the scale/time benchmark stays
appropriately opt-in (skipIf).

Verified: tsc --noEmit clean; 69 unit tests (4 suites) + 210 C# integration
resolver tests pass; prettier clean; eslint 0 errors.

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

* perf(csharp): replace remaining O(A) .some dedup scans with seeded Sets

The using-static member-injection loop and the cross-namespace import loop both
de-duped via `bucketArr.some((b) => b.def.nodeId === ...)` — O(A) per item. Both
now use a per-file `Map<simpleName, Set<nodeId>>`, seeded lazily from the
augmentation bucket (capturing entries from earlier passes), matching the
global and named-namespace paths. Same dedup semantics, O(1) amortized.

Verified: tsc --noEmit clean; csharp-hooks unit (27) + C# integration resolver
(210) tests pass; prettier + eslint clean.

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

* fix(csharp): gate suffix-fallback import resolution to declared namespaces (#1881)

C# `using` directives were resolving via an ungated suffix match, so a BCL
using like `System.Threading.Tasks` matched a coincidental local `Tasks.cs`
and emitted spurious IMPORTS edges. Add a declared-namespace gate that only
permits suffix-fallback when the import plausibly refers to an in-repo
namespace (exact, immediate-parent-declared, or ancestor-of a declared
namespace anchored at an in-repo root). Both resolution legs — the legacy
DAG and the registry-primary scope resolver — thread the same evidence to
the gate, including the no-csproj path.

Declared namespaces are collected with #1905's comment/string-aware scanner
(extractCsharpStructureViaScanner, lazily imported) instead of a regex, so
`namespace` tokens in comments/strings can't seed phantom namespaces. Scan
truncation or unreadable subtrees fail OPEN (gate disabled) and are logged.

Stacked on #1905 (fix/csharp-namespace-scope-oom).

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

* fix(csharp): cap per-file size in namespace scan; fail open on skip (#1881)

scanCSharpProject read every .cs/.csproj in full with no size guard and
issued per-directory reads with no concurrency bound, an OOM/FD-exhaustion
vector on large or generated repos. Add an fs.stat size guard before each
read, reusing getMaxFileSizeBytes() (the same 512KB cap the Phase-1 walker
uses). An oversized or unreadable .cs now signals truncation so the #1881
suffix-fallback gate fails OPEN rather than wrongly suppressing an import
whose declaring namespace lived in the skipped file (previously a silent
return left the scan looking complete). Adds a size-cap scan test.

* fix(csharp): bound per-directory read concurrency in namespace scan (#1881)

The scan issued every .cs/.csproj read in a directory at once via
Promise.all, so in-flight file descriptors scaled with the largest
directory's file count. Issue reads in bounded windows (32, mirroring
the Phase-1 filesystem-walker) via Promise.allSettled; an unexpected
read/scan rejection now trips truncation (fail open) instead of
rejecting the whole scan. Behavior-preserving for namespace collection
(C# scope-resolution parity passes on both legs).

* style(csharp): apply prettier to #1881 files to clear quality/format gate (#1908)

Reflow hand-wrapped lines in scope-resolver.ts and the csharp integration
test that prettier collapses under printWidth 100. Formatting only, no
behavioral change; clears the failing quality/format CI gate.

* fix(csharp): stream namespace scan so large generated files don't disable the #1881 gate (#1908)

Code-review follow-up. The scan read each .cs fully into a string behind a
512KB size cap (the tree-sitter parse budget); a single larger generated file
(*.g.cs, EF/gRPC output) tripped `truncated`, making the #1881 suffix-fallback
gate fail open repo-wide and silently undoing the fix on real repos.

Stream each .cs line-by-line via createReadStream + readline into a new
incremental scanner (createCsharpStructureScanner) instead of buffering the
whole file. Memory is now constant regardless of file size, so the per-file
size cap is dropped for the namespace line-scan and large generated files are
fully collected. extractCsharpStructureViaScanner is reimplemented on the same
incremental scanner (byte-identical; C# parity 2/2). collectDeclaredNamespaces
returns 'ok' | 'truncated' (truncation now only from an unreadable file) and the
truncation warn lists its real causes. csproj reads keep their size guard.

Prior art: ripgrep/ctags/Node readline stream rather than cap for line scans;
GitHub (384KB) and Sourcegraph (1MB) cap only their full-content indexes.

* fix(csharp): cap .csproj read via stream, not stat-then-read, to clear CodeQL TOCTOU (#1908)

CodeQL js/file-system-race flagged the fs.stat + fs.readFile size guard in
readCsprojConfig as a check-then-use filesystem race. Replace it with a
length-capped createReadStream (readFileTextCapped) — same memory bound on
untrusted input, no stat-then-read race, and consistent with the streamed
.cs scan. Behavior is unchanged for real .csproj files (parity 2/2).

* fix(csharp): keep BCL/external roots gated through scan truncation (#1908, Codex F1)

A single scan truncation (unreadable dir/file, depth/dir cap) set one
repo-wide `truncated` flag that made csharpSuffixFallbackAllowed fail
open for EVERY import, silently re-enabling the #1881 BCL->local suffix
matches. Add a CSHARP_EXTERNAL_ROOTS denylist (System/Microsoft/...): an
external-rooted using that does not align with an in-repo declared
namespace stays BLOCKED even under truncation, while genuinely
local-looking usings still fail open. A repo that declares the root is
allowed via the alignment escape hatch. Shared predicate, so both legs
inherit it.

* fix(csharp): gate the registry no-csproj direct-match path (#1908, Codex F2)

In the no-csproj branch of resolveCsharpImportTarget, resolveDirectMatch
ran BEFORE the gate, so a path-aligned Legacy/System/Threading/Tasks.cs
satisfied 'using System.Threading.Tasks;' even though System.* is not a
declared in-repo namespace — while the legacy leg (gate-first) blocked
it, so the legs were not equivalent. Run csharpSuffixFallbackAllowed
first (return null on fail), then direct-match, then progressive
stripping — mirroring the legacy ordering. Adds a no-csproj fixture with
a deep path-aligned Tasks.cs and dual-leg integration describes (registry
+ forced-legacy), plus a path-aligned unit case. Parity 2/2.

* fix(csharp): flag scanner-uncaptured namespaces incomplete; Unicode/@ matchers (#1908, Codex F3)

The line scanner treated its output as complete even when it missed valid
C# namespace forms, so the gate failed CLOSED and over-blocked legit
imports. Make CS_NAMESPACE_RE/CS_USING_STATIC_RE Unicode-aware (\p{L}\p{N}
+ u flag) and strip leading/segment @ so verbatim/Unicode identifiers are
captured to match the AST. For forms the regex still can't capture (split
across lines, not at line start, attributed), set a per-file 'incomplete'
flag; collectDeclaredNamespaces returns 'truncated' for such files so the
#1881 gate fails OPEN instead of dropping the namespace. High-precision
detectors + guard tests keep ordinary forms (incl. // namespace comments)
from tripping incomplete.

* fix(csharp): stream the .csproj RootNamespace read, no byte cap (#1908, Codex F4)

readCsprojConfig read only the first 512KB of a .csproj and, on a
match-miss, couldn't tell 'no RootNamespace' from 'RootNamespace past
the cap' — both synthesized a filename root. A wrong authoritative root
makes imports under the real root resolve to nothing AND suppresses the
fallback. Replace the capped read with a streamed early-stop search
(findCsprojRootNamespace) that reads until the tag or EOF: filename
fallback ONLY on genuine read-to-EOF absence; on a soft-budget cap-hit or
unreadable file, OMIT the config so the no-csproj fallback stays
reachable. Removes the now-unused readFileTextCapped + getMaxFileSizeBytes
cap from the scan. Parity 2/2.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:56:26 +01:00
Gergő MagyarandClaude Opus 4.8 f18ff521fc fix(group): stop Node gRPC loadPackageDefinition gate from matching every member call (#1916)
LOAD_PACKAGE_DEFINITION_SPEC matched `loadPackageDefinition` via a single
`function: [ (identifier) @fn (#eq?) (member_expression property:(property_identifier) @fn (#eq?)) ]`
alternation. Under the pinned tree-sitter@0.21.1 binding a top-level alternation
whose branches reuse one capture name collapses to a single pattern with a shared
predicate bucket: the member-expression branch's `@fn` is left unbound and its
`#eq?` is never enforced, so that branch matches EVERY `obj.method(...)` call
(`console.log(...)`, `logger.info(...)`, …). Since virtually every TS/JS file has
some member call, the `usesLoadPackage` gate was effectively always-open and
`new pkg.<Capitalized>Service(...)` was emitted as a spurious gRPC consumer — the
exact false positive the gate was added to prevent.

Split the spec into two single-branch PatternSpecs; each compiles to its own
Parser.Query with an independent predicate bucket where the `#eq?` is enforced
correctly. `runCompiledPatterns` concatenates their matches, so the
`.length > 0` gate is unchanged. `mk` now accepts a spec or a spec array.

Adds test_extract_ts_qualified_ctor_without_loadPackageDefinition_is_ignored, a
negative regression test verified to FAIL on the pre-fix code and PASS with the
fix: a file with no loadPackageDefinition but an unrelated member call +
`new authProto.auth.v1.AuthService(...)` must emit no consumer.

grpc-extractor suite 65/65; tsc + prettier + pre-commit hook clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:31:36 +01:00
5d710413d7 feat(group): extract OpenFeign @RequestLine consumer contracts (#1904)
* feat(group): extract OpenFeign @RequestLine consumer contracts

Adds Java HTTP plugin support for the native OpenFeign annotation
`@RequestLine("METHOD /path")`. Previously only `@FeignClient` interfaces
using Spring MVC method annotations (`@GetMapping` etc.) were detected;
the native annotation form — required by Feign Builder users and
non-Spring Feign deployments — was silently ignored.

Implementation:

- New `FEIGN_REQUEST_LINE_PATTERNS` covers both positional and named-arg
  (`value =`) forms.
- New `parseRequestLine()` parses the verb+path string and drops any
  query string (consistent with how RestTemplate/WebClient consumers
  handle inline literal URLs).
- The enclosing interface MUST carry `@FeignClient`; otherwise the
  detection is dropped to avoid false positives from same-named
  annotations in unrelated libraries.
- Reuses the existing `feignPrefixByInterfaceId` map so
  `@FeignClient(path=)` and `@RequestMapping` interface prefixes apply
  uniformly across both Spring MVC and `@RequestLine` methods.
- Confidence 0.75 — slightly higher than the 0.7 used for Spring MVC
  annotations because the verb is a string-literal value, not inferred
  from the annotation name (less ambiguous).

Six new unit tests cover: basic two-method extraction; `@FeignClient(path=)`
  prefix joining; query-string stripping; rejection of `@RequestLine` on
  non-Feign interfaces; mixing with `@GetMapping` on the same interface;
  named-argument form (`value = "..."`).

Verification: `npx tsc --noEmit`, full `test/unit/group` (31 files / 563
tests), `http-route-extractor.test.ts` (83/83 incl. 6 new), `prettier
--check` and `eslint` on touched files all pass.

* refactor(group): collapse @RequestLine positional + named-arg into one query

Per @magyargergo's review on PR #1904 — uses tree-sitter alternation
`[(...) (...)]` so the positional and named-argument forms of the
`@RequestLine` annotation are matched by a single compiled query and
invoked through one `runCompiledPatterns` pass instead of two.

* refactor(group): drop framework prefixes from java http pattern constant names

Per review feedback on #1904 — renames the four route-mapper pattern
constants to framework-agnostic names (the per-constant comments already
document which framework each targets):
  SPRING_TYPE_PREFIX_PATTERNS     -> TYPE_PREFIX_PATTERNS
  FEIGN_REQUEST_LINE_PATTERNS     -> REQUEST_LINE_PATTERNS
  FEIGN_INTERFACE_PREFIX_PATTERNS -> INTERFACE_PREFIX_PATTERNS
  SPRING_METHOD_ROUTE_PATTERNS    -> METHOD_ROUTE_PATTERNS

* refactor(group): collapse Java route-mapper annotations into one query

Merge the four annotation pattern bundles (Spring @RequestMapping type
prefix, @FeignClient(path) prefix, @(Get|Post|Put|Delete|Patch)Mapping
method routes and native @RequestLine) into a single
JAVA_ROUTE_ANNOTATION_PATTERNS query, read by scanRouteAnnotations() in
exactly one matches() pass per file. Variants are tagged by branch-local
captures and discriminated in JS (METHOD_ANNOTATION_TO_HTTP,
isRouteMemberKey), per review feedback. This drops the per-file annotation
passes from 4->1 in scan() and 2->1 in collectSpringTypes(), and removes
the interface-@RequestMapping / @FeignClient prefix redundancy.

Verb and path/value key filtering stay in JS rather than in-query: under
the pinned tree-sitter 0.21.1 binding a top-level [...] alternation
compiles to one pattern whose text predicates share a single bucket keyed
by capture name. A #match? against a capture absent from the matched
branch evaluates FALSE and silently drops every sibling-branch match,
whereas #eq? against an absent capture is vacuously true. So only fixed
annotation names use in-query #eq? (on branch-local captures); the
variable verb name and member key carry no in-query predicate.

Behaviour is unchanged for all compilable Java; existing http-route tests
(93) and the full group suite remain green.

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

* refactor(group): make Java route-annotation query generic, match name in loop

Collapse JAVA_ROUTE_ANNOTATION_PATTERNS from 9 annotation-name-pinned
branches to 6 generic structural branches (class/interface/method x
positional/named) that capture the annotation name (@ann), declaration
(@node), argument (@value) and member key (@key) generically. The query
now carries NO #eq?/#match? predicates at all; scanRouteAnnotations reads
@ann.text and @node.type in its for-loop to decide what each match means
(RequestMapping prefix, FeignClient(path) prefix, @(Get|...)Mapping route,
or @RequestLine), ignoring unrecognised annotations.

This makes the query framework-agnostic and extensible — adding a new
route annotation is a change to the loop and the lookup maps, not the
query — and removes the last tree-sitter-0.21.1 shared-predicate-bucket
footgun, since a predicate-free alternation cannot drop sibling branches.

Behaviour is byte-identical: 93 targeted http-route tests and the full
569-test group suite stay green; tsc and prettier clean.

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

* test(group): pin newly-reachable Java route-annotation JS branches; clarify invariants

Code-review follow-up to the route-annotation query consolidation. No
behaviour change to the extractor:

- Add two regression tests for branches the generic predicate-free query
  made reachable in scanRouteAnnotations: (1) a @RequestLine whose named
  argument is not `value` must be dropped (the in-query `#eq? @key "value"`
  guard now lives in JS); (2) @FeignClient(path) must win over @RequestMapping
  even when @RequestMapping is the first annotation in source order, covering
  the deferred interfaceRequestMappingPrefixes apply (the existing precedence
  test only covered @FeignClient-first).
- Document two invariants flagged in review: why prefixByTypeId and
  feignPrefixByInterfaceId intentionally diverge for the same interface node
  (Spring provider vs OpenFeign consumer prefix), and that the query's
  single-string-argument shape excludes array-valued annotations.

http-route-extractor + multi-verb suites: 95/95 (was 93); tsc + prettier clean.

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

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 08:50:29 +01:00
dependabot[bot] bef3da59a7 chore(deps)(deps): bump node-addon-api from 8.7.0 to 8.8.0 in /gitnexus (#1911)
Bumps [node-addon-api](https://github.com/nodejs/node-addon-api) from 8.7.0 to 8.8.0.
- [Release notes](https://github.com/nodejs/node-addon-api/releases)
- [Changelog](https://github.com/nodejs/node-addon-api/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nodejs/node-addon-api/compare/v8.7.0...v8.8.0)

---
updated-dependencies:
- dependency-name: node-addon-api
  dependency-version: 8.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-30 06:23:31 +01:00
azizur100389andGergő Magyar e234dac849 feat(cpp): add template partial ordering (#1885)
* feat(cpp): add template partial ordering

* fix(cpp): harden template partial ordering

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-29 21:38:19 +01:00
26894d835b fix(csharp): eliminate namespace-siblings OOM and worker-path re-parse (#1905)
* fix(csharp): eliminate O(S·D) BindingRef OOM in namespace siblings

Types declared in the C# global (default) namespace are visible from
every file, so the previous per-scope augmentation materialized
O(scopes × defs) BindingRefs — on large Unity solutions (tens of
thousands of global types) this caused severe slowness and OOM.

Route global-namespace types through a single workspace-level binding
channel (workspaceFqnBindings, consulted by lookupBindingsAt) for O(D)
memory. Also fix quadratic costs in the non-global path: append defs in
place instead of copying (was O(D²) per bucket), pre-index the first
scope per file (was O(S²·D)), and seed de-dup sets instead of repeated
.some scans.

Add csharp-pipeline-benchmark.test.ts (mirrors the PHP benchmark) with
spread and concentrated-global-namespace scenarios to track elapsedMs,
peakHeapMB, nodeCount, and edgeCount. Post-fix runs show linear scaling
and stable heap.

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

* perf(csharp): scanner fallback for namespace siblings on the worker path

Worker threads can't return tree-sitter Trees across MessageChannels, so
the cross-phase tree cache is empty for worker-parsed files. The C#
same-namespace pass (populateCsharpNamespaceSiblings -> extractFileStructure)
then re-parsed every file with tree-sitter to find namespace / using-static
nodes — effectively parsing a large solution a second time during scope
resolution.

Add a line-scanner fallback (extractCsharpStructureViaScanner) used only
when no cached Tree is available, mirroring PHP's fix for issue #1741. It
extracts the same namespaces / usingStaticPaths the AST walk produces for
the common line-anchored forms (file-scoped + block namespaces, plain /
global / aliased `using static`). The AST walk stays authoritative on the
sequential / warm-cache path.

Micro-benchmark over 3000 synthetic files: scanner is ~188x faster than
parse+walk (0.001 vs 0.251 ms/file) with identical output on the parity
spot-check; real-world files are larger, so the worker-path saving is
bigger. Adds csharp-namespace-extraction.test.ts (12 cases) covering all
declaration forms plus negative cases (using var, plain using, comments).

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

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

* fix(csharp): cover global-namespace workspaceFqnBindings path + doc + using-static perf

Addresses the production-readiness review of the namespace-siblings OOM fix.

- Add a unit test proving global-(default-)namespace C# types route to
  indexes.workspaceFqnBindings (one entry per simple name) with ZERO
  bindingAugmentations — pinning the O(D) invariant behind the #1871
  Unity-scale OOM fix and guarding against a revert to per-scope
  O(scopes x defs) augmentation. (The csharp-hooks mock now supplies
  workspaceFqnBindings, which the global fast path reads directly.)
- Correct the workspaceFqnBindings doc comment: it is shared by PHP
  (backslash-FQN keys) and C# (global-namespace simple-name keys); the two
  key formats are disjoint.
- Pre-index parsedFiles by path before the `using static` member-injection
  loop, replacing an O(files) find-per-import with an O(1) Map lookup.

Verified: tsc --noEmit clean; csharp-hooks + csharp-namespace-extraction
suites pass (38 tests); prettier clean; eslint 0 errors.

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

* fix(csharp): apply PR-review polish to namespace-siblings (tests, types, docs)

Addresses the multi-agent code review of this PR — the concrete, defensible
findings. Two items intentionally deferred (below).

- namespace-siblings.ts: couple the augmentation bucket + its de-dup set into
  one nullable lifecycle, removing the seen!/bucketArr! non-null assertions
  (identical runtime, still lazy).
- validate-bindings-immutability.ts: extend the dev-mode immutability validator
  to the third channel (workspaceFqnBindings) + a test; complete the validator
  test mock with workspaceFqnBindings.
- walkers.ts: document that namesAtScope deliberately excludes the
  scope-independent workspaceFqnBindings channel (enumerating workspace names at
  every scope would flood per-scope callers; lookupBindingsAt still consults it
  when resolving a specific name).
- scope-resolution-indexes.ts: reframe the workspaceFqnBindings doc to describe
  the key-format contract language-neutrally (examples, not language branching).
- csharp-hooks.test.ts: assert workspace entries carry origin:'namespace'; add a
  partial-class test (same simple name, distinct nodeIds across global files →
  both kept); rename the stale "parses" cache-miss test to "scans".
- csharp-pipeline-benchmark.test.ts: clearTimeout the Promise.race budget timer
  (dangling handle when the pipeline won the race).
- csharp.test.ts: correct the #1066 comment — extractFileStructure no longer
  re-parses on cache miss (line scanner); only emitCsharpScopeCaptures re-parses.

Deferred (surfaced, not applied): (1) worker-path scanner mis-reads
namespace/using-static inside block comments and verbatim/raw strings — an
explicitly documented trade-off mirroring the PHP scanner; hardening it to track
comment/string state is a separate decision. (2) workspaceFqnBindings is read
via an `as Map` cast; a type-safe mutable handle from finalize-orchestrator is a
cross-module contract change.

Verified: tsc --noEmit clean; 49 unit tests pass (incl. 3 new); prettier clean;
eslint 0 errors.

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

* fix(csharp): harden worker-path scanner + localize workspace-map cast

Addresses the two deferred PR-review findings plus the remaining test gap.

#1 — Worker-path scanner false positives: the line scanner now tracks block-
comment and string state across lines (advanceCsScanState), so a `namespace` /
`using static` keyword at the start of a line inside a block comment, verbatim
string (@"..."), or raw string literal ("""...""") is no longer mistaken for a
declaration on the worker cache-miss path. It matches only at code-state line
starts. 5 new scanner tests cover the block-comment / raw / verbatim cases.

#4 — workspaceFqnBindings type safety: the ReadonlyMap->Map cast is localized
to one documented line, and global-namespace writes go through a new
getWorkspaceBucket helper (mirroring getAugmentationBucket) rather than an
inline `.set()` at the mutation site.

#2 — lookupBindingsAt workspace-channel coverage: walkers-augmentations.test.ts
now exercises the third (workspace) channel: workspace-only, append-after-
finalized/augmented, and dedup-loses-to-finalized/augmented precedence.

#5 — OOM CI guard: the deterministic O(D) invariant (zero per-scope
augmentation for global types) is already asserted by the always-on
csharp-hooks unit tests added earlier; the scale/time benchmark stays
appropriately opt-in (skipIf).

Verified: tsc --noEmit clean; 69 unit tests (4 suites) + 210 C# integration
resolver tests pass; prettier clean; eslint 0 errors.

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

* perf(csharp): replace remaining O(A) .some dedup scans with seeded Sets

The using-static member-injection loop and the cross-namespace import loop both
de-duped via `bucketArr.some((b) => b.def.nodeId === ...)` — O(A) per item. Both
now use a per-file `Map<simpleName, Set<nodeId>>`, seeded lazily from the
augmentation bucket (capturing entries from earlier passes), matching the
global and named-namespace paths. Same dedup semantics, O(1) amortized.

Verified: tsc --noEmit clean; csharp-hooks unit (27) + C# integration resolver
(210) tests pass; prettier + eslint clean.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 20:24:58 +01:00
Gergő MagyarandClaude Opus 4.8 2a5bbbeaae fix: make extension installs offline-first (#1161)
* feat(review): add PR reviewer swarm agents

Seven read-only subagents coordinated by an orchestration skill for
structured, evidence-grounded production-readiness PR reviews.

Agents: facts-historian, branch-hygiene, risk-architect, test-ci-verifier,
security-boundary, docs-dod, synthesis-critic. All use Read/Grep/Glob/Bash
only — no edit tools.

Skill invoked as /gitnexus-pr-swarm-review <PR>.

* fix: patch vector extension and uncaughtException for review findings

- Add { policy: 'auto' } to both loadVectorExtension() calls in
  embedding-pipeline.ts so analyze --embeddings auto-installs VECTOR
- Add void to uncaughtException shutdown(1) call for Node v20+ safety
- Re-add getExtensionInstallPolicy export + default change + 4 tests

* fix(mcp,lbug): graceful shutdown exit codes + complete offline-first VECTOR policy

Completes the two live issues PR #1161 only partially addressed.

#1132 — MCP shutdown crash: SIGINT/SIGTERM were registered with `shutdown`
directly, so Node passed the signal NAME string into process.exit(), crashing
with ERR_INVALID_ARG_TYPE ('SIGTERM'). Map signals to numeric exit codes
(SIGINT->130, SIGTERM->143) via a testable installSignalShutdown(); add an
unref'd force-exit watchdog so a hung disconnect()/close() cannot wedge
shutdown; and void the stdin/stdout handlers so event payloads never reach
process.exit() as a non-number.

#1153 — offline-first extension loading:
- semanticSearch (a query/read path) no longer forces policy:'auto'; queries
  use load-only and never spawn a network INSTALL (extension.ladybugdb.com).
- the analyze embedding WRITE path resolves the policy from
  GITNEXUS_LBUG_EXTENSION_INSTALL (honoring never/load-only/auto; default auto)
  instead of hard-forcing 'auto', so an offline/locked-down operator's override
  is respected (the regression that re-broke #1153 for the VECTOR path).
- surface the active install policy in `gitnexus doctor` (was claimed but never
  delivered; also gives the previously-dead getExtensionInstallPolicy a caller).
- emit an actionable message when VECTOR is unavailable.

Tests: regression for the signal->numeric mapping (reproduces the signal-string
crash condition) and for embedding install-policy resolution. tsc/prettier clean,
eslint 0 errors, 55 unit tests pass.

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

* fix(analyze): degrade gracefully when FTS extension is unavailable

The load-only default made `gitnexus analyze` throw when the FTS
extension was not pre-installed, breaking CI and offline use. Make the
analyze write path opt into the `auto` install policy (LOAD-first then
bounded INSTALL — symmetric with the VECTOR/embeddings path and the #726
contract) and degrade gracefully when the extension still cannot load:
skip search-index creation, log a warning, and complete with a fully
queryable graph (only full-text/BM25 search is disabled). `--repair-fts`
still fails loudly.

- Surface the degraded state instead of reporting healthy:
  AnalyzeResult.ftsSkipped, a persistent CLI summary warning, and
  meta.json capabilities.fts.status = "unavailable".
- Skip the FTS-primitive integration tests when the extension is
  unavailable (shared skipUnlessFtsAvailable helper).
- Add a unit test for the degradation branch; fix the existing
  full-analyze test mock that omitted loadFTSExtension.

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

* test(lbug): skip FTS-seeding suites when extension is unavailable

The withTestLbugDB helper seeds FTS indexes in beforeAll via createFTSIndex,
which throws when the optional FTS extension cannot load — failing the whole
suite on machines where it is neither pre-installed nor installable (the
macOS platform-sensitive CI runner). Probe the extension once (mirroring the
analyze write path's `auto` policy), bypass FTS seeding when it is
unavailable, and skip the suite's tests via beforeEach with a one-time
warning so the skip is visible rather than a setup crash.

Fixes the macOS failures in search-core, search-pool, local-backend-calltool,
and staleness-and-stability. Suites still run normally where FTS is available.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 20:04:41 +01:00
252fbabd51 fix(ingestion): stop emitting phantom Function defs for array-method callbacks (#1906)
* fix(ingestion): stop emitting phantom Function defs for array-method callbacks

The HOC-wrapped-arrow scope-query pattern (`const X = HOC(args => ...)`),
added for React idioms such as forwardRef/memo/useCallback, also matched
array higher-order-method callbacks like `const x = arr.map(a => ...)`.
Those produced a spurious `@declaration.function` named after the
binding, on top of its value def, so calls inside the callback attributed
to a phantom `Function:x` instead of the enclosing scope.

- Add a shared `isArrayMethodCallbackArrow` detector
  (`ARRAY_CALLBACK_METHODS` blocklist) and suppress the
  `@declaration.function` emit-side in both the JS and TS scope-captures
  emitters, leaving the value binding as the sole def.
- Add `selectNodeBearingDef` in scope-extractor: the tested
  collapse-rule contract (function-like > value > first) the deferred
  node-creation migration will consume to keep one graph node per
  binding.

This corrects the registry-primary scope model and CALLS-edge
attribution (calls inside array-method callbacks now source from the
enclosing File scope). The duplicate graph *node* itself is still
created by the legacy parse-worker path and is removed by the follow-up
node-creation migration.

Refs #1876

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

* test(ingestion): strengthen array-callback coverage; document receiver-blind suppression

Follow-ups from the production-readiness review of PR #1906:

- array-callback.ts: document that isArrayMethodCallbackArrow is
  receiver-blind — an in-set method name on a NON-array receiver
  (Map/Set.forEach, RxJS observable.map, query-builder .sort, lodash
  chain .filter) is also suppressed. Accepted limitation, not a bug:
  the binding holds the call's result value, not a callable.
- captures unit tests (JS + TS): add a non-array-receiver
  characterization case, and extend the it.each lists to cover
  findLast, findLastIndex, reduceRight — the full 13-entry
  ARRAY_CALLBACK_METHODS set is now exercised in both languages.
- js-array-method-callback-attribution integration test: tighten the
  File-sourced CALLS assertions from toBeGreaterThan(0) to
  toHaveLength(1) (now also catches over-attribution).
- scope-extractor.ts: note that the dead selectNodeBearingDef export is
  intentional and tracked by #1876 (deferred node-creation migration).

Comment-and-test only; no production behavior change. Verified locally:
tsc clean, prettier/eslint clean, captures unit 106 passed,
scope-extractor 31 passed, integration 3 passed.

Refs #1876

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:35:06 +01:00
Gergő MagyarandClaude Opus 4.8 85727ca625 feat(review): add PR reviewer swarm agents (#1851)
* feat(review): add PR reviewer swarm agents

Seven read-only subagents coordinated by an orchestration skill for
structured, evidence-grounded production-readiness PR reviews.

Agents: facts-historian, branch-hygiene, risk-architect, test-ci-verifier,
security-boundary, docs-dod, synthesis-critic. All use Read/Grep/Glob/Bash
only — no edit tools.

Skill invoked as /gitnexus-pr-swarm-review <PR>.

* Address PR review feedback (#1851)

- Pin explicit model IDs in all 7 reviewer-swarm agents per CLAUDE.md
  (no unversioned aliases). Set the two mechanical agents
  (test-ci-verifier, branch-hygiene-reviewer) to claude-haiku-4-5-20251001
  per @Cenrax's "this could be haiku"; the five analytical agents use
  claude-sonnet-4-6.
- Add an explicit read-only Bash policy (permitted/prohibited command
  lists) to every agent's Rules section, so the read-only guarantee is
  defended against injected/adversarial PR content rather than prose-only.
- Add a hard synthesis-critic gate to the swarm skill: do not post the
  final review until the critic's "Required corrections before posting"
  section is empty (was advisory only).

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

* feat(review): make PR reviewer swarm portable across AI CLIs

Restructure the reviewer swarm around a single CLI-neutral source of truth so it
runs from any AI CLI, not just Claude Code.

- pr-swarm-review/: canonical orchestration.md (Swarm + Solo execution modes with
  an identical output contract) and personas/0N-*.md (the 7 review personas,
  relocated verbatim from the Claude agents, each tagged with a model tier and the
  read-only Bash policy). Single source of truth — edit here, not in the wrappers.
- Thin per-CLI adapters that read the canonical spec at runtime (no duplication):
  - Claude Code: coordinator skill (Swarm mode) + the 7 agents are now thin
    wrappers that read their persona file (frontmatter/model preserved; mechanical
    lanes Haiku, analytical lanes Sonnet).
  - Gemini CLI: .gemini/commands/gitnexus-pr-swarm-review.toml
  - GitHub Copilot: .github/prompts/gitnexus-pr-swarm-review.prompt.md
  - Cursor: .cursor/commands/gitnexus-pr-swarm-review.md
- AGENTS.md: canonical "PR Swarm Review" section -> orchestration.md, the universal
  entrypoint honored by Codex, Cursor, Gemini, Copilot, and any AGENTS.md-aware
  agent (Codex user-level prompt install noted in the README).

Graceful degradation: only Claude Code has parallel subagents (Swarm mode); every
other CLI runs the 7 lanes sequentially in one agent (Solo mode) with the same
output contract. prettier --check clean (root config).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 18:24:16 +01:00
4bc8622642 fix(group): derive grpc consumer FQN from java imports for client-jar consumers (#1889)
* fix(group): derive grpc consumer FQN from java imports so client-jar consumers don't fall back to short names

Java gRPC microservices commonly follow the "client-jar" pattern: the
service owner publishes a pre-compiled stub jar to a Maven repository
and consumer repos depend on the jar instead of carrying the
originating `.proto` files. gRPC's official Java quickstart, Alibaba
HSF, ByteDance KiteX-Java and google-cloud-java all document this
shape.

Before this commit, `GrpcExtractor` resolved a fully-qualified
contract id (`grpc::<package>.<Service>/*`) only when the consumer
repo also carried a matching `.proto`. Client-jar consumers had no
proto, so they fell back to a short-name contract id
(`grpc::<Service>/*`) that never matched the provider's contract id.
Cross-repo grpc cross-link counts dropped to zero on every realistic
Java microservice group — including all of crsdp's `crsdp-backend →
unipus_cloud_framework` connections.

Fix: derive the proto package directly from each consumer file's
`import <pkg>.<XxxGrpc>;` statement. The package from the import is
exactly the proto package, so the contract id matches the provider's
verbatim — no `.proto` lookup needed in the consumer repo.

Implementation
--------------

* `grpc-patterns/types.ts` — `GrpcDetection` gains an optional
  `protoPackage` field. Plugins set it when the package can be
  derived from the source file alone.
* `grpc-patterns/java.ts` — adds `GRPC_CLASS_IMPORT_PATTERNS`, a
  tree-sitter query that captures every
  `import_declaration > scoped_identifier { scope, name }` pair where
  the imported name ends in `Grpc`. `import static …` and
  `import w.x.*;` are excluded by tree-sitter shape: the `name:` field
  is only present on the non-static, non-wildcard form. The plugin
  builds a per-file `XxxGrpc → fullPackage` map and tags every
  provider / consumer detection it emits.
* `grpc-extractor.ts` — `detectionToContract()` now resolves the
  contract id in three steps:
    1. detection-supplied `protoPackage` wins (skips the proto map
       entirely so an unrelated same-name service in the consumer
       repo can't blur the FQN);
    2. otherwise consult the legacy per-repo proto map;
    3. otherwise fall back to a short-name contract id, preserving
       pre-fix behaviour.
  Confidence stays at the "with proto" tier when the import path
  resolves: an import statement in real source is at least as
  authoritative as a per-repo proto map.

Same-short-name disambiguation
-------------------------------

The motivating case `unipus_cloud_framework` defines two distinct
`ContentRpcService` services in different proto packages
(`cn.unipus.ucf.api.proto.client.service.ContentRpcService` vs
`cn.unipus.ucf.admin.proto.client.service.ContentRpcService`). Two
consumer files importing the two flavours now emit two distinct FQNs;
neither could be told apart from the other under the legacy short-
name fallback.

Out of scope
------------

`import w.x.*;` (wildcard service imports) are left to the legacy
short-name fallback. Wildcard imports are discouraged by Google's
Java style guide and IntelliJ's defaults, and resolving them
unambiguously would require either group-level proto-package
catalogs or per-class disambiguation, both of which are larger
follow-ups. This commit only changes behaviour for the dominant
specific-import case.

Tests
-----

`test/unit/group/grpc-extractor.test.ts` adds a new "Java client-jar
consumer (import-derived FQN)" describe block with 9 cases covering
both the happy paths (consumer/provider FQN derivation, same-short-
name disambiguation, import-vs-local-proto precedence) and the
regression-protection paths (no import + no detection emitted, static
imports / wildcards ignored, mixed-file repos preserved).

End-to-end verification
-----------------------

Ran the patched cli on the real `crsdp-backend` (consumer, no
`.proto`) and `unipus_cloud_framework` (provider, has `.proto`)
repos. Synced as a two-repo group, every `XxxGrpc` referenced via a
specific import in `UcfAdminGrpcClientService.java` produced an FQN
contract id that exact-matched the provider repo's FQN — 9 grpc
cross-links surfaced where there were 0 before.

Verification
------------

* `npx tsc --noEmit`: pass
* `npx tsc` (dist rebuild): pass
* `test/unit/group/grpc-extractor.test.ts`: 60/60 pass (51 existing
  + 9 new)
* `test/unit/group/`: 30 files / 545 tests all green
* `npx prettier --check` on touched files: pass
* `npx eslint` on touched src files: 0 errors / 0 warnings

* fix(group): handle option java_package and proto-map disagreement in grpc detection

Addresses Claude bot review on PR #1889:

- Finding 1: parse `option java_package` when building proto context;
  add a reverse index so an import-derived package can be translated
  back to the proto package.
- Finding 2: when same-repo proto map has the service, use the proto
  package; warn and record `meta.importPackage` if the import disagrees.
- Finding 3: add an end-to-end wildcard match test (provider+consumer
  fixture, runs `buildProviderIndex`+`runWildcardMatch`).

Client-jar consumer + diverging `java_package` (no local proto)
remains a known limitation; pinned by a dedicated test.

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-29 14:55:28 +01:00
23bf594a70 fix(standalone): wire standalone providers into scope-extractor for registry-primary (COBOL Ring 3 flip) (#1842)
* feat(cobol): migrate COBOL to scope-based resolution (regex provider)

Migrate COBOL to scope-based registry resolution, validating the
parse-source-agnostic contract — COBOL uses regex, not tree-sitter,
but implements the same LanguageProvider interface via emitScopeCaptures.

Phase 1-5 complete per #941 DoD.

New files:
  languages/cobol/captures.ts       — emitScopeCaptures wrapping regex tagger
  languages/cobol/interpret.ts      — import/type-binding/receiver hooks
  languages/cobol/index.ts          — barrel export
  languages/cobol/scope-resolver.ts — ScopeResolver wiring (9 fields, 3 toggles)

Modified files:
  languages/cobol.ts                — wire 4 scope-resolution hooks
  registry.ts                       — register cobolScopeResolver
  registry-primary-flag.ts          — document REGISTRY_PRIMARY_COBOL

Fixtures:
  17 fixture files, 30 test cases across 11 required classes
  test/integration/resolvers/cobol-scope.test.ts

Tests: 24/24 pass (default + REGISTRY_PRIMARY_COBOL=0)
tsc: zero cobol-specific errors
Shadow mode (GITNEXUS_SHADOW_MODE=1): zero crashes
Regex perf: 10K-line file in 408ms (threshold: 2000ms)

NOT added to MIGRATED_LANGUAGES — REGISTRY_PRIMARY_COBOL env var only.

* chore(cobol): add COBOL to MIGRATED_LANGUAGES

* fix(cobol): revert MIGRATED_LANGUAGES flip, fix JSDoc dup, fix arityCompatibility

* fix(standalone): wire standalone providers into scope-extractor for registry-primary (COBOL Ring 3 flip)

- Gate cobolPhase with isRegistryPrimary() guard to prevent double emission
- Wire standalone providers (parseStrategy !== 'tree-sitter') with
  emitScopeCaptures into parse-worker via extractParsedFile bridge
- Add COBOL to MIGRATED_LANGUAGES in registry-primary-flag.ts
- Fix Module scope range in captures.ts to use full program bounds
  (was just PROGRAM-ID line, causing scope containment failures)
- Update cobol.test.ts grand totals to be mode-aware
- Wrap legacy exact-count assertions in if (!isPrimary)
- Fix cobol-scope.test.ts fixture path to use __dirname (was process.cwd())

Tests:
  REGISTRY_PRIMARY_COBOL=0: 83/83 pass (59 legacy + 24 capture)
  REGISTRY_PRIMARY_COBOL=1: 28/28 pass (4 mode-aware + 24 capture)

* test(cobol): restore original test assertions, add mode-aware describe blocks alongside

- Remove if (!isPrimary) wrapper from legacy assertions
- Keep ALL 59 original tests intact and running unconditionally
- Add new 'scope-resolution mode' describe block alongside legacy tests
- New block uses isPrimary to check for scope-resolution capture output
- Legacy tests run against cobolPhase output (skipGraphPhases=true)
- Mode-aware tests validate standalone provider wiring in registry-primary mode

* fix(test): use result.graph instead of result.parsedFiles in scope-mode test

- PipelineResult has no parsedFiles field; use graph.nodes instead
- Use toBe strict equality (not.toBeNull()) per review feedback
- Object.keys for node count as suggested by reviewer

* test(cobol): add COBOL pipeline benchmark following PHP benchmark structure

- Generate synthetic COBOL codebases at 100/250/500 file scales
- Each file has 1 PROGRAM-ID, N paragraphs, cross-file CALLs, COPY books
- Measures wall-clock time, peak heap, node/edge counts
- SkipIf(!GITNEXUS_BENCH) — run with GITNEXUS_BENCH=1
- Prints table with scaling ratios and linearity assertions

* fix(bench): remove COPY from paragraphs, add REGISTRY_PRIMARY_COBOL note

- COPY statements belong only in DATA DIVISION (already present there)
- Revert copyLine inside paragraph blocks to idiomatic COBOL
- Add header note about =1 mode producing ~0 node/edge counts

* fix(bench): restore COPY in paragraphs for preprocessing stress

- COPY in paragraph blocks exercises the preprocessor expansion path
  more heavily than DATA DIVISION only placement.

* fix(bench): constant 3 paragraphs per program, add 1000-files scale, relax threshold to 4x

- Fixed paragraphsPerProgram to constant 3 for consistent scaling
- Added 1000-file scale to benchmark
- Raised assertion threshold to 4x to accommodate 100-250 step

* fix: skip standalone providers in scope-resolution phase when registry-primary

scopeResolutionPhase was reading all COBOL files from disk and running
scope-resolution for standalone providers that don't emit graph edges
yet. Added a guard: if provider.languageProvider.parseStrategy ===
'standalone', skip it entirely. Saves 68s at 1000 files in =1 mode.

* fix: remove COBOL isRegistryPrimary gate, suppress standalone IMPORTS double-emission

- Remove the isRegistryPrimary gate in cobolPhase so it runs in both modes,
  keeping cobolPhase as the sole COBOL graph-edge producer.
- Add a guard in runScopeResolution to skip emitImportEdges for standalone
  providers (parseStrategy === 'standalone'), preventing scope-resolution
  from duplicating IMPORTS edges already produced by cobolPhase.
- Scope-resolution still runs for standalone providers (capture extraction,
  model finalization, reference resolution) — only edge emission is skipped.
- Both modes: 60/60 cobol.test.ts, 24/24 cobol-scope.test.ts.

* fix: 4 review fixes — dead code removal, memory cleanup, benchmark comment, standalone-bridge test

1. Remove dead standalone guard in run.ts (phase.ts:164 is canonical).
2. Filter standalone preExtractedByPath entries in phase.ts (memory leak).
3. Update benchmark comment: cobolPhase runs in both modes.
4. Add unit test proving extractParsedFile works for COBOL standalone provider.
   Revert PipelineResult.parsedFiles — not needed with unit test approach.

* perf(cobol): memoize copybook preprocessing; make benchmark measure file-count scaling

The COBOL pipeline benchmark reported superlinear (quadratic) scaling, but the
pipeline itself is O(n) in file count. The superlinearity was a fixture artifact:
every program COPYed all floor(fileCount/5) copybooks in WORKING-STORAGE, so
emitted data-item nodes — and total work — grew O(n^2). Verified empirically:
node count grew ~2x per file-doubling; with constant per-program fan-out it grows
exactly 1x (linear), and 0/3 adversarial audits could refute the O(n) conclusion.

- benchmark: each program now COPYs a constant 3 shared copybooks so the
  benchmark measures true file-count scaling. Add a deterministic node-ratio
  assertion that fails if the O(n^2) copy-all fan-out is reintroduced.
- processor: memoize preprocessed copybook content per processCobol call so each
  copybook is preprocessed once, not once per COPY site
  (O(programs x copybooks) -> O(copybooks)). Safe: REPLACING is applied later by
  the expander on the cached pre-REPLACING content.

Verified: 246 COBOL tests pass; benchmark scales linearly (node ratio 1.0); tsc clean.

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

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:25:19 +01:00
2f15c1ece1 feat(group): add Kotlin Spring WebClient long-form HTTP consumer extraction (#1884)
* feat(group): add Kotlin Spring WebClient long-form HTTP consumer extraction

Follow-up to #1855. Extends `kotlin.ts` with the long-form WebClient
fluent chain that #1855 explicitly deferred:

  webClient.method(HttpMethod.GET).uri("/x").retrieve().awaitBody<T>()

This pattern remains common in Kotlin Spring 4 → 5 migrations and in
codebases that prefer the fluent verb-as-enum style. The short form
(`webClient.get().uri("/x")`) was already supported in #1855.

Approach:
  - Single deeper tree-sitter query (`WEB_CLIENT_LONG_PATTERNS`) that
    matches the full chain structurally — both `.method(HttpMethod.X)`
    and `.uri("...")` in one pattern. Verb is captured as the
    `simple_identifier` of the `HttpMethod.X` field access.
  - Verb is whitelisted to GET/POST/PUT/DELETE/PATCH (consistent with
    the short-form's `WEB_CLIENT_SHORT_TO_HTTP` map).
  - Receiver constraint `(#eq? @obj "webClient")` mirrors the short
    form and Java plugin heuristic.

Out of scope (intentional):
  - Variable-bound verbs: `val verb = HttpMethod.PATCH; webClient.method(verb)...`
    Source-scan can't follow the binding without graph context.
    Pinned by an anti-overreach test.
  - HEAD/OPTIONS/TRACE: not in `WEB_CLIENT_SHORT_TO_HTTP` either —
    keeps polyglot symmetry with java.ts and the short form.

Tests: 4 new cases under `consumer extraction — fetch patterns`,
gated by tree-sitter-kotlin grammar availability.

  positive (3)
   - long form GET
   - long form POST / PUT / DELETE / PATCH (4 verbs in 1 fixture)
   - no double-emit pin (long-form chain produces exactly one
     consumer, not one from each query)
  anti-regression (1)
   - variable-bound verb does NOT match (graph-aware concern)

The previous `'does NOT match Kotlin WebClient long form (deferred
to follow-up)'` test from #1855 is replaced by these — the deferred
state is now resolved.

Reverse-validated: temporarily disabling the long-form emit makes
exactly the 3 positive tests fail; the variable-bound-verb anti-
regression test continues to pass (it pins behavior independent
of the emit being on or off).

Local validation:
  - test/unit/group/http-route-extractor.test.ts: 66/66 ✅
  - test/unit/group: 546/546 ✅
  - npx prettier --check (changed files): clean ✅

* test(group): address Claude review findings F1 and F2 on PR #1884

Two minor follow-ups from the production-readiness review:

F1 — Stale block comment at the top of the Kotlin consumer suite
(was: "Three consumer flavors covered here ... long-form deferred
to a follow-up"). Updated to "Four consumer flavors" and removed
the deferred sentence — the deferral is resolved by this PR. The
kotlin.ts file header was already updated; this brings the test
file comment in sync. Per DoD §2.3 (no stale comments).

F2 — Replaced `expect(wcConsumers.length).toBeGreaterThanOrEqual(4)`
with `expect(wcConsumers).toHaveLength(4)` in the multi-verb test.
The fixture is fully deterministic — exactly 4 long-form calls,
no other consumer types — so an exact count assertion is the right
shape per DoD §2.7 ("use toBe / toEqual for exact expectations").
Added a comment explaining what the assertion catches that the
existing per-verb toBeDefined() checks would miss (accidental 5th
consumer from a duplicate query firing or a regressed receiver
constraint).

F3 (HEAD/OPTIONS/TRACE negative test) is intentionally not added
in this PR — same precedent as #1855 where HEAD/OPTIONS/TRACE on
the short form are also implicitly excluded without a pinning
test. Happy to add one in a separate PR if maintainers want
explicit pinning across both forms.

F4 (CI on pre-merge SHA) is the maintainer's call — the merge from
main is theirs to re-trigger CI on. The merge brings only Java
consumer changes (PR #1872) and Go provider changes (PR #1886),
both in entirely separate files from this PR's Kotlin work.

Local validation:
  - test/unit/group/http-route-extractor.test.ts: 73/73 ✅
    (66 from this PR pre-merge + 7 from PR #1872 merged via main)
  - npx prettier --check (changed files): clean ✅

* refactor(group): hoist Kotlin WebClient long-form verb regex to module scope

Address @magyargergo's review request on PR #1884:

  > Can you please extract the regexp from the for loop? 🙏
    (kotlin.ts:510)

Compiles the verb whitelist `^(GET|POST|PUT|DELETE|PATCH)$` once at
module load instead of every iteration of the long-form scan loop.
Mirrors the placement and JSDoc style of the sibling
`WEB_CLIENT_SHORT_TO_HTTP` constant.

Behavior is unchanged — same verb whitelist, same exclusion of
HEAD/OPTIONS/TRACE for symmetry with the short form. The 4
itKotlinConsumer long-form tests added in this PR continue to
pass, and the variable-bound-verb anti-overreach test continues
to pin the deliberate non-match.

Local validation:
  - test/unit/group/http-route-extractor.test.ts: 77/77 ✅
  - test/unit/group: 557/557 ✅
  - npx prettier --check (changed file): clean ✅

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-29 09:27:54 +01:00
JaysonAlbertandgfwangjie 7dae4fcc41 fix(group): attribute Spring interface routes to controllers (#1743)
* fix(group): attribute Spring interface routes to controllers

* test(group): normalize Spring route fixture paths

---------

Co-authored-by: gfwangjie <gfwangjie@gf.com.cn>
2026-05-29 07:35:01 +01:00
evolutionandGergő Magyar d71fd1688b feat(go): add builtInNames set to Go language provider (#1886)
* feat(go): add builtInNames set to Go language provider

Add GO_BUILT_INS (15 functions, 18 types, 3 values) to the Go
LanguageProvider for parity with the other 13 language providers.
The set is converted to an isBuiltInName predicate by defineLanguage()
and consumed by the type-env return-type lookup to short-circuit
lookups for Go built-in symbols.

* feat(go): add Go 1.18+ and 1.21 predeclared identifiers to builtInNames

Add `clear`, `min`, `max` (Go 1.21 builtins), `any`, `comparable`
(Go 1.18 type aliases), and `iota` (predeclared constant) to
GO_BUILT_INS for complete coverage of the Go specification.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-29 06:46:02 +01:00
MyShining 7b38b8aae2 feat(java): add HTTP consumer contract extraction (#1872) 2026-05-29 06:05:40 +01:00
b565c7c990 feat(ingestion): resolve FastAPI include_router(prefix=...) cross-file routes (#1877)
* feat(ingestion): resolve FastAPI include_router(prefix=...) cross-file routes

FastAPI sub-route files declare paths via @router.<verb> while the entry
file mounts the router with app.include_router(<router>, prefix='/x').
Previously both the ingestion-layer Route graph nodes and the group-layer
ExtractedContract URLs lost the cross-file prefix, breaking provider <->
consumer matching.

Ingestion layer:
  - parse-worker emits routerIncludes / routerImports + decoratorReceiver
  - parsing-processor / parse-impl thread the new fields and aggregate
    prefixesByModule across chunks; decorator routes whose receiver is
    'router' are duplicated once per matching prefix
  - routes.ts joins prefix via normalizeExtractedRoutePath

Group layer:
  - HttpLanguagePlugin gains an optional prepareRepo() pre-pass and a
    repoContext arg to scan(); python.ts builds prefixesByModule and
    falls back to the bare path when no entry matches
  - http-route-extractor caches one repoContext per plugin

Tests:
  - 3 new http-route-extractor cases (attr / named-import / no-prefix)
  - ParseWorkerResult literals in 3 test files updated to the new shape

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

* fix(ingestion,group): address PR #1877 review — relative imports, cross-package collisions, host names, ingestion tests

Follow-ups to the FastAPI `include_router(prefix=...)` cross-file fix
based on PR #1877's automated production-readiness review. Three
correctness gaps and one test coverage gap addressed:

1. Relative-import support in the worker regex (FINDING 2)
   `FROM_IMPORT_ROUTER_RE` now accepts module paths starting with a
   `.` (e.g. `from .calls import router as calls_router`). The
   previous `[A-Za-z_][\w.]*` rejected leading dots and silently
   dropped every relative-import Shape-B include — a real pattern
   from the PR description's own motivating example. The matching
   helpers now strip leading dots before keying so absolute and
   relative imports collapse to the same module key.

2. Cross-package same-name module collisions (FINDING 3)
   Two-tier module keying replaces the previous basename-only key:
     • short key — `users`            (file basename without `.py`)
     • long  key — `api/users`        (parent dir + stem)
   `prefixesByLongKey` is consulted first and only falls back to
   `prefixesByShortKey` when no long-key match is available. Both
   the ingestion pipeline (parse-impl.ts) and the group extractor
   (http-patterns/python.ts) carry the same scheme so the graph
   nodes and HTTP contracts agree on which prefix applies.

   New protocol field `ExtractedRouterModuleAlias` (parse-worker →
   parsing-processor → parse-impl) lets Shape-A
   `<host>.include_router(<mod>.router, prefix='/x')` calls promote
   to a long key when the same file imports `<mod>` via
   `from <pkg> import <mod>`. Without this, `api/users.py` and
   `admin/users.py` collided on the basename `users` and the admin
   file's routes inherited the `/users` prefix that was only meant
   for `api/users.py`.

3. Non-`app` host variable names (FINDING 4)
   The group-layer `INCLUDE_ROUTER_*_PATTERNS` queries pinned the
   host identifier to the literal `"app"` and dropped every
   `application = FastAPI()` / `api = FastAPI()` pattern — the
   constraint was redundant given that the call shape
   (`include_router` invoked with a router argument and a
   `prefix=` keyword) is already specific enough. The pin is
   removed; the ingestion regex was already unrestricted.

4. Ingestion-layer regression tests (FINDING 1)
   The previous PR added group-layer tests
   (`http-route-extractor.test.ts`) but zero in-tree tests for the
   ingestion path. Two new suites pin the
   worker → parse-impl → routes flow:

   - `test/unit/fastapi-router-bindings.test.ts` (23 cases):
     `extractFastAPIRouterBindings()` is split into a stand-alone
     module so it can be unit-tested without booting a worker
     thread, then pinned for regex shape, two-tier key emission,
     relative-import support, and negative cases.
   - `test/integration/fastapi-prefix-pipeline.test.ts` (5 cases)
     plus `test/fixtures/fastapi-prefix-app/` — runs the full
     `runPipelineFromRepo()` against a realistic multi-package
     fixture (containing both `api/users.py` and `admin/users.py`)
     and inspects the resulting `Route` graph nodes for cross-file
     prefix joining and absence of cross-package bleed.

Verification

  - `npx tsc --noEmit`: pass
  - PR-touched test suites (6 files / 117 cases): all green
  - `npx prettier --check`: pass on touched files
  - `npx eslint`: 0 errors on touched files

Cache / compatibility

  The new `routerModuleAliases?` field on `ParseWorkerResult` and
  `routerModuleAliases` on `WorkerExtractedData` are optional /
  guarded with `?? []`, so historical parse-cache entries continue
  to load without forced re-scan.

Refs PR #1877.

* refactor(ingestion): move fastapi-router-bindings out of workers/ — pure module, not a worker

Addresses @magyargergo's `CHANGES_REQUESTED` review on PR #1877:

> Sorry I just found that we are introducing a new worker in the PR.

`gitnexus/src/core/ingestion/workers/fastapi-router-bindings.ts` was a
**pure-function module** — it never imported `worker_threads` or
`parentPort`, never spawned a worker, and was never registered as a
worker entry. It was placed in `workers/` purely because it was split
out of `workers/parse-worker.ts` to make its functions unit-testable
without booting a worker thread (parse-worker is itself the worker
entry and cannot be loaded from the main thread).

To remove the misleading directory placement:

  • The implementation moves to
    `gitnexus/src/core/ingestion/route-extractors/fastapi-router-bindings.ts`,
    alongside the other framework-specific route extractors (`expo`,
    `nextjs`, `php`, `laravel`, `middleware`, `response-shapes`).
  • `workers/parse-worker.ts` keeps a thin re-export so the worker
    entry can keep using `extractFastAPIRouterBindings` directly. The
    re-export now carries an explicit comment stating that the imported
    file is **not** a worker and that the `workers/` directory
    deliberately hosts only true worker entries (`parse-worker.ts`,
    `worker-pool.ts`, `quarantine.ts`).
  • The new file's leading docstring opens with "NOT A WORKER" and
    explains why it exists where it does.
  • The unit test (`test/unit/fastapi-router-bindings.test.ts`) is
    updated to import from the new path.

No behaviour change. The function body, signatures, and exported types
are identical.

Verification

  • `npx tsc --noEmit`: pass
  • `npx tsc` (dist rebuild): pass
  • `test/unit/fastapi-router-bindings.test.ts` (23 cases): all green
  • `test/integration/fastapi-prefix-pipeline.test.ts` (5 cases): all green
  • `test/unit/group/http-route-extractor.test.ts` (63 cases): all green
  • `npx prettier --check` on touched files: pass
  • `npx eslint` on touched files: 0 errors

Refs PR #1877.

* refactor(ingestion): drop parse-worker re-exports; consumers import router types directly from route-extractors

Addresses @magyargergo's two remaining review comments on PR #1877:

1. **`gitnexus/src/core/ingestion/workers/parse-worker.ts:247`** —
   "Can you please remove them and update the call sites?"

   The `export type { ExtractedRouterInclude, ExtractedRouterImport,
   ExtractedRouterModuleAlias } from '../route-extractors/...'` block
   in parse-worker.ts is gone. The remaining `import type {…}` is
   purely local — used only to type the corresponding fields on
   `ParseWorkerResult` below — and the leading comment now says so
   explicitly ("this file does NOT re-export them"). The
   `extractFastAPIRouterBindings` symbol is also no longer re-exported
   from parse-worker.ts; it's still imported here so the worker entry
   can call it per file, but downstream consumers must reach it via
   `route-extractors/fastapi-router-bindings` directly.

   Call sites updated:
     - `gitnexus/src/core/ingestion/parsing-processor.ts`
     - `gitnexus/src/core/ingestion/pipeline-phases/parse-impl.ts`

   Both files now `import type { ExtractedRouterInclude,
   ExtractedRouterImport, ExtractedRouterModuleAlias }` directly from
   `route-extractors/fastapi-router-bindings.js`. The worker types
   they still need (`ParseWorkerResult`, `ExtractedToolDef`, etc.)
   keep coming from `workers/parse-worker.js`.

   The unit + integration tests already imported from the new path,
   so no test changes were required.

2. **`gitnexus/src/core/ingestion/parsing-processor.ts:168`** —
   suggested simplification:

       for (const item of result.routerIncludes ?? []) allRouterIncludes.push(item);
       for (const item of result.routerImports ?? []) allRouterImports.push(item);
       for (const item of result.routerModuleAliases ?? []) allRouterModuleAliases.push(item);

   Applied verbatim. Replaces the previous `if (result.…) for …`
   guards. The cache-compat semantics are unchanged — historical
   parse-cache entries that lack these fields still load cleanly,
   the new form just spells the fallback inline.

No behavior change, no tests touched, no public API change.

Verification

  • `npx tsc --noEmit`: pass
  • `npx tsc` (dist rebuild): pass
  • PR-touched test suites (6 files / 117 cases): all green
  • `npx prettier --check` on touched files: pass
  • `npx eslint` on touched files: 0 errors

Refs PR #1877.

* refactor(ingestion): hoist fastapi-router-bindings type imports to top of parse-worker.ts

Move the `import type { ExtractedRouterInclude, ExtractedRouterImport,
ExtractedRouterModuleAlias }` block to the top of the file with the
other type imports, and drop the comment that previously sat next to
ExtractedDecoratorRoute.

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 19:04:19 +01:00
azizur100389andGergő Magyar 97c1f85e87 refactor(cpp): Use function-type ADL entities (#1822)
* fix(cpp): use function-type ADL entities

* test(hooks): stabilize concurrency burst reporting

* Fix C++ return type capture subtag handling

* Harden C++ function-type ADL extraction

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 17:18:19 +01:00
11fc43b425 feat(impact): per-symbol processes field on byDepth items (#1867)
* feat(impact): per-symbol processes field on byDepth items

Today `impact` returns aggregated `affected_processes` at the top level
but the per-symbol `byDepth` items don't say which processes each caller
participates in. Consumers planning a deploy want to know if a given
caller is hit by a daily cron, a webhook, or a user-facing route - each
is a different deploy-risk profile - and that information requires a
follow-up cypher query per symbol today.

This change attaches `processes: [...]` to every `byDepth[depth][i]`
item, listing the processes that symbol participates in:

  byDepth: {
    "1": [
      {
        depth: 1,
        id: "Function:src/foo.ts:doStuff",
        name: "doStuff",
        ...
        processes: [
          { id: "proc:cron_daily", label: "Daily cron",
            processType: "cron", step: 12 }
        ]
      }
    ]
  }

The list is empty for symbols not in any process. Additive change, no
breaking modifications to existing fields.

Implementation:
- A second chunked Cypher pass runs after the existing per-process
  aggregation pass, returning per-(symbol, process) rows. Same chunk
  size and MAX_CHUNKS as the aggregation pass, so worst-case adds 10
  extra round-trips bounded by the same env var.
- The enrichment pass is skipped entirely when `affectedProcesses.length
  === 0` (nothing to enrich) or `summaryOnly === true` (byDepth not
  returned anyway).
- The aggregation query is unchanged - the new query has a distinct
  RETURN shape (`RETURN s.id AS sid, ...`) so an existing unit test that
  counts STEP_IN_PROCESS chunks was narrowed to match only the
  aggregation pattern.

Tests:
- New: byDepth items always have a `processes` field (default empty
  when no STEP_IN_PROCESS edges exist).
- New: when STEP_IN_PROCESS rows exist, the matching byDepth item
  carries the right `{id, label, processType, step}` entry.
- Updated: impact-batching-grouping test mock narrowed to count only
  aggregation chunks (the new per-symbol pass is covered separately).

* style: apply prettier to gitnexus/src/mcp/local/local-backend.ts

Pure line-wrap fix flagged by quality / format CI on PR #1867. Zero
semantic change: prettier broke a chained .slice().map() across three
lines instead of one. No test changes, no logic changes.

* fix(impact): address PR review findings on per-symbol process enrichment

- byDepth.processes doc now states each item carries processes (Finding 1)
- move per-symbol STEP_IN_PROCESS enrichment post-pagination so symbols
  beyond the pre-pagination cap no longer get false-empty processes:[]
  (Finding 2); hoist CHUNK_SIZE/MAX_CHUNKS to function scope so the
  post-pagination pass can reference them
- dedup per-symbol query with DISTINCT + MIN(r.step) per (symbol,process)
  pair (Finding 3)
- suppress the per-symbol pass under summaryOnly, incl. impactByUid group
  fan-out, plus a test asserting the query never fires (Findings 4, 6)

* fix(impact): address second-round review findings A-E

Finding A (blocker): impactByUid passed summaryOnly:true, which drops the
entire byDepth field. cross-impact.ts reads fan.byDepth to build the group
by_depth output, so cross-repo by_depth was always {}. Replace with a new
skipPerSymbolEnrichment option on _runImpactBFS that suppresses only the
per-symbol STEP_IN_PROCESS pass while preserving byDepth.

Finding B+D (blocker): rewrite the byDepth.processes tool description. Drop
the stale "enrichment cap" wording (no longer true post-pagination), document
the {id,label,processType,step} entry shape, and tell agents to cross-check
affected_processes when partial:true.

Finding C: bound the post-pagination per-symbol enrichment loop to
MAX_CHUNKS*CHUNK_SIZE page IDs and surface partial:true when capped, so a
large page cannot trigger unbounded DB round-trips (DoD 2.6).

Finding E: add a test exercising the real impactByUid -> _runImpactBFS path
asserting byDepth survives and the per-symbol query never fires.

---------

Co-authored-by: scotjelinski <58397194+scotjelinski@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 16:15:37 +01:00
50715e3894 chore(deps)(deps-dev): bump @playwright/test in /gitnexus-web (#1860)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.58.2 to 1.60.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.58.2...v1.60.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Abhigyan Patwari <126312502+abhigyanpatwari@users.noreply.github.com>
2026-05-28 07:51:05 +01:00
dependabot[bot]andGergő Magyar ca95df6316 chore(deps): bump github/codeql-action from 4.35.4 to 4.35.5 (#1866)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 06:46:18 +01:00
dependabot[bot]andGergő Magyar 9d609cc386 chore(deps)(deps): bump axios from 1.16.0 to 1.16.1 in /gitnexus-web (#1864)
Bumps [axios](https://github.com/axios/axios) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 06:45:30 +01:00
dependabot[bot]andGergő Magyar 128a199970 chore(deps)(deps-dev): bump @types/node in /gitnexus-web (#1863)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.9.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 06:45:18 +01:00
dependabot[bot]andGergő Magyar 76409783aa chore(deps)(deps): bump @langchain/langgraph in /gitnexus-web (#1861)
Bumps [@langchain/langgraph](https://github.com/langchain-ai/langgraphjs/tree/HEAD/libs/langgraph-core) from 1.2.9 to 1.3.2.
- [Release notes](https://github.com/langchain-ai/langgraphjs/releases)
- [Changelog](https://github.com/langchain-ai/langgraphjs/blob/main/libs/langgraph-core/CHANGELOG.md)
- [Commits](https://github.com/langchain-ai/langgraphjs/commits/@langchain/langgraph@1.3.2/libs/langgraph-core)

---
updated-dependencies:
- dependency-name: "@langchain/langgraph"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-28 06:45:01 +01:00
Gergő Magyar 99168be773 feat(ingestion): trace indirect call patterns — FastAPI Depends() and frontend HTTP consumers (#1852) 2026-05-28 05:33:30 +01:00
henry201605andhenry d9d6318b64 feat(group): add Kotlin Spring HTTP consumer extraction (#1855)
* feat(group): add Kotlin Spring HTTP consumer extraction

Follow-up to #1849 (Kotlin providers). Extends `http-patterns/kotlin.ts`
with three call-site patterns common in Kotlin Spring projects:

  - RestTemplate: `restTemplate.getForObject("/x", ...)` and the
    full verb family (getForObject/getForEntity → GET,
    postForObject/postForEntity → POST, put → PUT, delete → DELETE,
    patchForObject → PATCH). Mirrors the Java plugin's
    `REST_TEMPLATE_TO_HTTP` map so polyglot repos coalesce on a
    single contract id.

  - WebClient short form: `webClient.get().uri("/x")` and the
    `.post()` / `.put()` / `.delete()` / `.patch()` siblings. The
    chain parses as two nested `call_expression` nodes; the query
    anchors on the outer `.uri(...)` and walks one level inward
    to constrain the verb.

  - OkHttp: `Request.Builder().url("/x")`. Kotlin parses
    `Request.Builder()` as a `call_expression` whose callee is a
    `navigation_expression` (not Java's `object_creation_expression`),
    so the query shape differs from `java.ts` but the receiver/method
    constraints (`Request` / `Builder` / `url`) and emitted
    contract format match.

Out of scope: `webClient.method(HttpMethod.X).uri("/y")` long form.
The verb sits on a sibling `call_expression` two hops away, so it
needs a walk-up helper rather than a flat tree-sitter query. A
dedicated anti-overreach test pins the current behavior so a future
short-form change can't accidentally start matching the long form.

Receiver name constraints (`#eq? @obj "restTemplate"`,
`#eq? @cls "Request"`) match the Java plugin's heuristic — a project
that aliases the receiver under a different name won't be picked up.
This trade-off keeps false-positive rates low and is documented in
the file header.

Tests: 5 new cases under `consumer extraction — fetch patterns`,
gated by tree-sitter-kotlin grammar availability.

  positive (3)
   - RestTemplate verbs (5 calls × 5 verbs)
   - WebClient short-form verbs (5 calls × 5 verbs)
   - OkHttp Request.Builder().url("/x")
  anti-regression (2)
   - WebClient long form `.method(HttpMethod.X)` produces no
     consumer (deferred-feature pin)
   - non-restTemplate receiver does not match (receiver-name pin)

Reverse-validated: removing the `(#eq? @obj "restTemplate")`
constraint causes the receiver-name anti-regression test to fail.

Local validation:
  - test/unit/group/http-route-extractor.test.ts: 59/59 ✅
  - test/unit/group: 539/539 ✅
  - npm run format:check: clean ✅

* test(group): pin Kotlin OkHttp POST-chain heuristic-default GET behavior

Address Claude review on PR #1855 (Finding 1).

The OkHttp query in `kotlin.ts:OK_HTTP_PATTERNS` matches the
`.url("/x")` sub-expression of a builder chain, but the verb is
encoded on a separate sibling call (`.post(body)` / `.delete()` /
...). The query intentionally does not walk the chain to recover
the verb — it emits `method: 'GET'` for every match, mirroring the
Java plugin's `OK_HTTP_PATTERNS` (java.ts).

Concretely: `Request.Builder().url("/x").post(body).build()` becomes
`http::GET::/x`, not `http::POST::/x`. This is an already-accepted
Java parity heuristic, but it was untested on the Kotlin side.

This commit:
  - Adds an anti-overreach test pinning the current behavior:
      * exactly one consumer is emitted with method=GET
      * no second http::POST::/x consumer appears
  - Documents the limitation in kotlin.ts as a "Known limitation"
    block tied to the test, so a future verb-walk implementation
    has to update the comment in lockstep with the assertion.

Rationale for not implementing verb-walk in this PR:
  - Verb-walk requires walking sibling call_expression nodes (the
    `.post(body)` chain), which is the same shape as the
    deferred WebClient long-form work
  - Java has the same limitation in production today; fixing only
    Kotlin would create polyglot drift
  - A coordinated future PR can add verb-walk to both plugins at
    once and update both comments + the pin tests together

Finding 2 (silent test-skip when tree-sitter-kotlin grammar is
unavailable) is intentionally NOT addressed here — same gating
pattern was accepted in #1849 for Provider tests, and a coordinated
follow-up should add a CI sentinel covering both Provider and
Consumer suites in one place.

Local validation:
  - test/unit/group/http-route-extractor.test.ts: 60/60 ✅
  - test/unit/group: 540/540 ✅
  - npm run format:check: clean ✅

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
2026-05-27 21:32:24 +01:00
henry201605andhenry 46eb0ebf56 feat(group): add Kotlin Spring HTTP route extraction (named + positional) (#1849)
* feat(group): add Kotlin Spring HTTP route extraction (named + positional)

Mirror the Java Spring named-argument fix for Kotlin Spring Boot
controllers. Adds a new `http-patterns/kotlin.ts` plugin behind the
optional `tree-sitter-kotlin` grammar, registered for `.kt`/`.kts`.

Both annotation forms produce providers:
  @RequestMapping("/api")          / @GetMapping("/users")
  @RequestMapping(path = "/api")   / @GetMapping(value = "/users")
  @RequestMapping(value = "/api")  / @GetMapping(path = "/users")

The Kotlin AST (fwcd/tree-sitter-kotlin) shares one node type
(`value_argument`) for positional and named forms, so the queries
are split:
  - positional: anchors `string_literal` as the first named child
    of `value_argument` via the immediate-child anchor `.`
  - named: explicitly captures `simple_identifier` and constrains
    it to `^(path|value)$` via `#match?`, mirroring the same
    safety bar enforced by `http-patterns/java.ts` and
    `topic-patterns/java.ts`. Without this constraint the query
    would also capture non-route attributes like `produces`,
    `consumes`, `headers`, `name`, `params`.

`tree-sitter-kotlin` is an optionalDependency (parser-loader.ts,
parse-worker.ts pattern). When the native binding is unavailable
the plugin exports `null` and `index.ts` skips registering
`.kt`/`.kts` so the orchestrator stays healthy.

Scope: providers only. Consumer detection (RestTemplate, WebClient,
OkHttp) on Kotlin call-site ASTs differs enough from Java's
`method_invocation` shape to warrant a separate, focused PR.

Tests: 11 new cases under `provider extraction — source-scan
fallback (Strategy B)`, gated by the kotlin grammar availability.

  positive (8)
   - class @RequestMapping("/api/v1") (positional)
   - class @RequestMapping(path = "/api/v2")
   - class @RequestMapping(value = "/orders")
   - method @GetMapping(value = "/users")
   - method @GetMapping(path = "/users")
   - method @PostMapping(path = "/users")
   - mixed: class named-arg + method positional
   - mixed: class positional + method named-arg
  anti-regression (3)
   - @GetMapping(produces = "application/json") emits no provider
   - @GetMapping(name = "x", value = "/users") emits exactly one provider
   - @RequestMapping(path = "/api", name = "myApi") prefix stays /api

Reverse-validated: removing the `(#match? @key "^(path|value)$")`
constraint causes precisely the 3 anti-regression tests to fail.

Local validation:
  - test/unit/group/http-route-extractor.test.ts: 54/54
  - test/unit/group: 534/534
  - npx tsc --noEmit: clean (modulo the pre-existing TS2339 in
    user-defined-conversions.ts merged from main, unrelated)

* style(test): apply prettier line wrapping to long itKotlin titles

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
2026-05-27 09:33:52 +01:00
eeea46466b fix(group): handle named annotation args in Java Spring route extraction (#1834)
* fix(group): handle named annotation args in Java Spring route extraction

The Java HTTP plugin only matched positional `@RequestMapping("/path")`
syntax for class-level prefixes and method-level routes. Named argument
forms (`path = "/path"` and `value = "/path"`) produce an
`element_value_pair` AST node that the tree-sitter queries did not cover,
causing the class prefix to be lost and named-arg method routes to be
missed entirely during cross-repo contract extraction.

Add a second pattern to both SPRING_CLASS_PREFIX_PATTERNS and
SPRING_METHOD_ROUTE_PATTERNS matching the element_value_pair structure.

* fix(group): constrain Spring named-arg query to path/value keys + add regression tests

Address Claude review on PR #1834. The named-argument patterns added
in 8b6fa6e used `value: (string_literal)` (a tree-sitter field
selector for the right-hand side of element_value_pair), which matched
ANY annotation member with a string value — not just `path`/`value`.

Concrete fallout (without this fix):
  @GetMapping(produces = "application/json") → bogus http::GET::/application/json
  @GetMapping(name = "listUsers", value = "/users") → extra http::GET::/listUsers
  @RequestMapping(headers = "X-Foo=bar", path = "/api") → class prefix
    could be set to "X-Foo=bar" because prefixByClassId.set runs per
    match in document order, so the LAST element_value_pair wins.

The sibling topic-patterns/java.ts already demonstrates the correct
shape: constrain the `key:` field to the route member names.

This commit:
  - Adds `key: (identifier) @key (#match? @key "^(path|value)$")` to
    both SPRING_CLASS_PREFIX_PATTERNS and SPRING_METHOD_ROUTE_PATTERNS
    named-arg queries.
  - Adds 9 regression tests under
    `provider extraction — source-scan fallback (Strategy B)`:
      * @RequestMapping(path = "/api/v3") class prefix
      * @RequestMapping(value = "/orders") class prefix
      * @GetMapping(value = "/users") method route
      * @PostMapping(path = "/users") method route
      * mixed: class named-arg + method positional
      * mixed: class positional + method named-arg
      * @GetMapping(produces = "application/json") → no provider emitted
      * @GetMapping(name = "listUsers", value = "/users") → exactly one
        provider with path "/users", no /listUsers route
      * @RequestMapping(path = "/api", name = "myApi") → prefix is /api,
        not myApi (verifies the class-prefix overwrite scenario)

Tests: 42/42 pass in http-route-extractor.test.ts;
       522/522 pass under test/unit/group;
       npx tsc --noEmit clean.

* test(group): add @GetMapping(path = ...) case to match review checklist verbatim

Claude review on PR #1834 explicitly asked for the method-level
`@GetMapping(path = "/users")` case. The previous commit covered it
indirectly by exercising path= on @PostMapping (the Spring method
annotations share the same query, so any verb proves the path= field
is matched). Add a dedicated GET+path= test so the reviewer's
checklist is satisfied 1:1, and keep the POST+path= case as a bonus
verb-coverage test.

Tests: 43/43 pass in http-route-extractor.test.ts.

---------

Co-authored-by: henry <zhangwei2017@unipus.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-27 07:35:46 +01:00
dependabot[bot]andGergő Magyar ca3e1755c2 chore(deps)(deps): bump lru-cache from 11.4.0 to 11.5.0 in /gitnexus (#1844)
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.4.0 to 11.5.0.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-lru-cache/compare/v11.4.0...v11.5.0)

---
updated-dependencies:
- dependency-name: lru-cache
  dependency-version: 11.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-27 06:45:18 +01:00
dependabot[bot]andGergő Magyar 6acdc49f06 chore(deps)(deps-dev): bump @types/node in /gitnexus (#1845)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.0 to 25.9.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-27 06:44:41 +01:00
azizur100389 b1445daf04 feat(cpp): rank user-defined conversions (#1829) 2026-05-27 06:36:23 +01:00
d903152eba fix(typescript): reuse suffix index in scope resolver (#1840)
* fix(typescript): reuse suffix index in scope resolver

Build a suffix index once per TypeScript scope-resolution pass and pass it into standard import resolution so package-style imports avoid repeated linear file-list scans.\n\nFixes #1839

* test(typescript): add wiring-level test for scope-resolver suffix index

- Test typescriptScopeResolver.resolveImportTarget directly (the real
  production entry point) with package-style, unresolvable, and relative
  imports
- Use vi.spyOn on buildSuffixIndex to verify the index is built inside
  the makeTsResolveImportTarget closure — fails if index wiring is removed
- Fix existing test to pass real file lists instead of empty arrays
  alongside the prebuilt index, matching production wiring

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
2026-05-26 20:23:13 +01:00
6c572749b0 fix(web): stop Nexus AI agent when user clicks Stop (#1820)
* fix(web): stop Nexus AI agent when user clicks Stop

Wire AbortController through chat streaming so Stop cancels the LangGraph
run instead of only hiding the loading UI. Fixes #1615.

* fix(web): address PR review feedback for Nexus AI stop

Guard stream cleanup against Stop-then-Send races, remove dead cancelled
handler, tighten abort error detection, add stopped tool-call status, and
extend abort unit tests. Fixes #1615.

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

* fix(web): address review findings for Nexus AI stop/cancel

- Fix race conditions in useAppState.tsx abort lifecycle:
  - Replace stale isChatLoading closure guard with chatStateRef
  - Track and cancel rAF handles in stopChatResponse/finally
  - Move cancelled chunk check before onChunk dispatch
  - Simplify finally block to unconditional cleanup via chatStateRef
  - Guard tool_result from overwriting stopped status
  - Have clearChat abort in-flight streams before clearing
- Reorder isAbortError to check error identity before signal.aborted
- Refactor AgentStreamChunk to discriminated union for exhaustive switch
- Fix test assertions to use exact .toEqual() per DoD §2.7
- Add test for plain Error with name AbortError
- Remove dead markStopped alias, simplify signal spread-conditional

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Test <test@example.com>
2026-05-26 19:18:36 +01:00
SparshandGergő Magyar 7556a8e73a feat(cobol): migrate COBOL to scope-based resolution (regex provider) (#941) (#1835)
* feat(cobol): migrate COBOL to scope-based resolution (regex provider)

Migrate COBOL to scope-based registry resolution, validating the
parse-source-agnostic contract — COBOL uses regex, not tree-sitter,
but implements the same LanguageProvider interface via emitScopeCaptures.

Phase 1-5 complete per #941 DoD.

New files:
  languages/cobol/captures.ts       — emitScopeCaptures wrapping regex tagger
  languages/cobol/interpret.ts      — import/type-binding/receiver hooks
  languages/cobol/index.ts          — barrel export
  languages/cobol/scope-resolver.ts — ScopeResolver wiring (9 fields, 3 toggles)

Modified files:
  languages/cobol.ts                — wire 4 scope-resolution hooks
  registry.ts                       — register cobolScopeResolver
  registry-primary-flag.ts          — document REGISTRY_PRIMARY_COBOL

Fixtures:
  17 fixture files, 30 test cases across 11 required classes
  test/integration/resolvers/cobol-scope.test.ts

Tests: 24/24 pass (default + REGISTRY_PRIMARY_COBOL=0)
tsc: zero cobol-specific errors
Shadow mode (GITNEXUS_SHADOW_MODE=1): zero crashes
Regex perf: 10K-line file in 408ms (threshold: 2000ms)

NOT added to MIGRATED_LANGUAGES — REGISTRY_PRIMARY_COBOL env var only.

* chore(cobol): add COBOL to MIGRATED_LANGUAGES

* Revert "chore(cobol): add COBOL to MIGRATED_LANGUAGES"

This reverts commit f234330e9f.

* fix(cobol): revert MIGRATED_LANGUAGES flip, fix JSDoc dup, fix arityCompatibility

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-26 18:37:39 +01:00
c8117d1292 feat(web): Introduce Tree View and Circles View in Web Viewer (#1799)
* feat(graph-view): add tree and circles layout modes

Add alternate graph layouts to the web viewer with new graph view state, canvas controls, adapters, and Sigma layout logic for tree and concentric-circle rendering. Include layout and adapter tests plus tree-view E2E coverage aligned with the English UI labels, and tune node visibility, edge layering, large-graph behavior, and tree-layer spacing so the new views stay readable. Follow up the tree-view work by keeping noisy variables hidden by default and mapping Property/Const icons so filter coverage stays in sync with the expanded node taxonomy.

Co-authored-by: OpenAI Codex <noreply@openai.com>
AI-model: GPT-5 Codex

* fix(web): cap tree layout spring iterations and remove unused variable

Finding A (blocker): calculateTreeLayout runs 14 synchronous spring
iterations over all edges and nodes — O(N×E×14) + O(N log N) per layer
per iteration — with no size guard.  At 10K+ nodes this freezes the
main thread for several seconds.

Fix: make SPRING_ITERATIONS adaptive:
  - N > 10 000 → 0 iterations (proportional initial layout only)
  - N >  3 000 → 4 iterations
  - otherwise  → 14 iterations (unchanged behaviour for small graphs)

Also removes the unused `const r` at useSigma.ts:1314, which was a
leftover after the radial-resistance decomposition was removed.
This clears the CodeQL "unused variable" warning (Finding G).

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* test(graph-adapter): add circles adapter tests and tree layout perf bound

Finding B (high): knowledgeGraphToCirclesGraphology had zero test
coverage.  Adds three new tests:

- ring placement: verifies Folder→ring 0, File→ring 1, Function→ring 3
  and confirms circles-specific attributes (circlesRing, circlesAnchorX/Y)
  are set while tree attributes (treeAnchorX/Y) are absent.
- edge styling: CONTAINS is marked isHierarchyEdge=true with the
  hierarchy colour; CALLS is cross-cutting with its own colour.
- CALLS cross-cutting: a lone CALLS edge between two Functions is
  correctly identified as a non-hierarchy edge.

Also adds a performance-bound test for the tree adapter at 2 000 nodes /
4 000 edges (the adaptive 14-iteration path) asserting completion within
2 s — catches regressions to the O(N×E×iterations) main-thread blocking
that Finding A identified.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* refactor(web): rename Tree View → Sequential Layout, Circles → Radial Layout

Aligns the UI labels with standard graph layout terminology from the
Cambridge Intelligence taxonomy (cambridge-intelligence.com/blog/automatic-graph-layouts):

  Tree View   → Sequential Layout  (顺序布局)
  Circles     → Radial Layout      (径向布局)
  Force Graph → Force Graph        (unchanged)

Internal graphViewMode keys ('tree', 'circles', 'force') are unchanged —
only the displayed strings in en/zh-CN locales and the E2E button selectors
are updated.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* perf(web): add adaptive large-graph guards to sequential layout physics

For graphs with N > 5 000 nodes, each rAF frame of runTreeLayout was
doing O(N log N) sort + O(N × k) repulsion pair comparisons (k ≈ 2 400
for a 20 K-node graph spread across 1 080 px at range 130).  At that
scale each frame took hundreds of ms, making the canvas appear completely
frozen even though the physics loop was still running.

Fix mirrors the circles layout adaptive strategy:

  N > 5 000 (large):
    - Skip repulsion pass (O(N × k) → 0)
    - Skip spread-force sort (O(N log N) → 0)
    - Velocity cap raised to ±12 / ±6 px so nodes cover ground faster
    - Damping 0.58, 1 sim step/frame, 30 s max duration
    - Looser early-stop thresholds (max v 0.05, avg v 0.03, active 2 %)

  N > 1 500 (medium):
    - Velocity cap raised to ±6 / ±3 px
    - 24 s max duration
    - Repulsion and spread still active

  N ≤ 1 500 (small):
    - Unchanged behaviour (velocity ±3/±2, 18 s, all forces active)

Layer gravity (O(N)) and edge springs (O(E)) run for all graph sizes —
they provide the structural pull that replaces repulsion at large N.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* fix(web): fix stale closure in sigma event handlers breaking node selection

The sigma 'clickNode', 'clickStage', 'enterNode', and 'leaveNode' handlers
are registered in a one-time useEffect (empty dep array). They captured
options.onNodeClick via closure, so they always called the initial version
of handleNodeClick — the one created before the graph loaded where
`if (!graph) return` exits immediately.

Consequence: clicking a node in the canvas never updated the app-level
selectedNode state. This broke:
  - The Focus Depth filter (warning "Select a node to apply depth filter"
    persisted even after a canvas click)
  - The depth hop filter not applying (selectedNode was always null)
  - The code panel not opening on canvas node click

Fix: store the three callback props in refs (onNodeClickRef, onNodeHoverRef,
onStageClickRef) and update them synchronously on every render. The sigma
event handlers now read from the refs, so they always invoke the latest
version of the callbacks without needing to re-register.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* fix(web): address three code-review bugs in graph rendering

Bug 1 (useSigma.ts): forces in the tree physics loop were computed once
before the sub-steps loop and reused for every step, causing 2× displacement
on slow frames (>64ms, simulationSteps>1). Fix: move forceX/forceY Maps and
all force accumulation (layer gravity, edge springs, repulsion, spread) inside
the loop so each sub-step integrates from current node positions.

Bug 2 (graph-adapter.ts): all three adapters used `graph.hasEdge(src,tgt)`
as a dedup guard, which silently drops any second edge between the same node
pair. A CALLS relationship between nodes that also have a CONTAINS edge was
always lost. Fix: switch from `new Graph()` to `new MultiGraph()` (allows
multiple edges per pair) and dedup by `rel.id` instead of by node pair.

Bug 3 (graph-adapter.test.ts): the cross-cutting edge styling test never
executed its CALLS branch because Bug 2 dropped the CALLS edge before the
assertion ran. Fix: assert `sigmaGraph.size === 2` and verify both edges
individually after collecting attrs by relationType.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-5

* fix(web): address three code-review bugs in graph rendering

- Move radial layout force accumulation inside the sub-step loop so
  forces are recomputed from updated node positions each iteration
  instead of using stale forces computed before the loop began
- Revert knowledgeGraphToGraphology from MultiGraph back to Graph with
  node-pair deduplication to prevent ForceAtlas2 from double-applying
  spring forces for node pairs that share multiple relation types
- Add Target to the lucide-icons import in FileTreePanel.tsx so the
  Const node type icon resolves without a ReferenceError

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* fix(web): address four more PR review comments

Edge visibility (useSigma.ts): HAS_METHOD / HAS_PROPERTY edges were hidden
when any edge-type filter was active because those types are not in the EdgeType
union. Normalize HAS_METHOD → DEFINES and HAS_PROPERTY → CONTAINS before the
visibleTypes.includes() guard so Kotlin/Java hierarchy edges follow the same
filter logic as their semantic equivalents.

Force-mode edge styles (graph-adapter.ts): HAS_METHOD / HAS_PROPERTY fell back
to the default gray color in the force-graph adapter because EDGE_STYLES had no
entries for them. Added explicit entries using the same hues as DEFINES/CONTAINS
so force mode renders Kotlin/Java hierarchy edges consistently with tree/circles.

Accessibility (GraphCanvas.tsx, locales): the layout-mode switcher (Force /
Tree / Circles) had no ARIA semantics. Added role="tablist" on the container
and role="tab" + aria-selected on each button. Added the viewModes.label i18n
key (used as aria-label on the tablist) to en and zh-CN locale files.

Flaky test (graph-adapter.test.ts): replaced the hard 2 s wall-clock assertion
with a structural check (node count + edge count) that is deterministic across
CI hardware. Timing tests are inherently flaky and provide no correctness signal.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-5

---------

Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-26 18:05:50 +01:00
681a352006 fix(worker): analyze native worker aborts (#1833)
* fix(analyze): avoid native aborts on generated worker bundles

Retire timed-out parse workers instead of force-terminating native parser state, and skip Monaco generated worker bundles by default while preserving explicit .gitnexusignore negation overrides.

Constraint: Node native tree-sitter bindings can abort the process when a timed-out worker is terminated while inside parser state.

Rejected: Falling back to sequential parsing for native stalls | it can move the same native crash onto the main thread.

Confidence: high

Scope-risk: moderate

Directive: Keep timeout recovery from force-terminating workers until they return to JS or exit naturally.

Tested: npm test; npx tsc --noEmit; npm run build; targeted analyze on /Users/wangxc/Code/keep; gitnexus detect_changes --scope staged

Not-tested: Node 22 LTS runtime and non-macOS platforms

* fix(worker): bound retired parser worker lifetimes

Keep timeout recovery from immediately terminating workers that may still be inside native parser state, while making terminal pool shutdown own retired worker cleanup so long-lived processes do not accumulate retired threads.

Constraint: Claude review on PR #1833 required retiredWorkers cleanup in pool.terminate() and tripBreaker() without regressing no-immediate-terminate timeout safety.

Rejected: clearing the retiredWorkers set without terminating | would remove JS bookkeeping while leaking the underlying worker thread.

Confidence: high

Scope-risk: moderate

Directive: Preserve the distinction between recoverable timeout retirement and terminal pool shutdown; do not reintroduce immediate terminate in removeWorkerFromSlot(..., 'retire').

Tested: npx vitest run test/unit/worker-pool-timeout-retire.test.ts; npx vitest run test/unit/worker-pool-timeout-retire.test.ts test/unit/worker-pool-resilience.test.ts test/unit/worker-pool-cumulative-timeout.test.ts test/unit/worker-pool-slot-generation.test.ts; npx tsc --noEmit; npm run build; npx prettier --check src/core/ingestion/workers/worker-pool.ts test/unit/worker-pool-timeout-retire.test.ts ../docs/todo/pr-1833-retired-worker-cleanup-plan.md; npx eslint src/core/ingestion/workers/worker-pool.ts test/unit/worker-pool-timeout-retire.test.ts; gitnexus detect_changes --scope staged.

Not-tested: npm test full suite did not complete green in this environment; two runs each had one unrelated test/unit/hooks.test.ts parseHookOutput null failure, and each failed hook test passed when rerun in isolation.

* ci: retrigger checks

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

---------

Co-authored-by: wangxc <wangxc_a_bj@si-tech.com.cn>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 17:28:39 +01:00
Gergő MagyarandTest 5e012c373b fix(cli): detect missing LadybugDB native binary at startup with actionable guidance (#835) (#1837)
* fix(cli): detect missing LadybugDB native binary at startup with actionable guidance (#835)

Add checkLbugNative() pre-flight that verifies lbugjs.node exists before
any command transitively imports @ladybugdb/core. When missing (bun default
install, --ignore-scripts), prints repair instructions instead of crashing
with ERR_DLOPEN_FAILED. Also enhances `gitnexus doctor` to probe the
native binary status.

* fix(review): guard eval-server, un-guard status command

eval-server transitively loads @ladybugdb/core and needs the native
binary check. status only reads filesystem metadata and should remain
accessible when the binary is missing.

* fix(lint): use console.log instead of console.error in native check gate

The project eslint config only allows console.log.

* fix(cli): route native-check to stderr and validate binary loadability

Fixes two Codex adversarial review findings:

1. Native-check failure message now goes to process.stderr.write instead
   of console.log, preventing MCP stdout protocol contamination.

2. checkLbugNative now attempts a controlled require() probe after the
   existence check. Truncated, ABI-mismatched, or wrong-platform binaries
   produce actionable guidance instead of passing through to crash at
   process.dlopen.

---------

Co-authored-by: Test <test@example.com>
2026-05-26 17:10:08 +01:00
Gergő MagyarandTest 05d269ec28 feat(ruby): migrate Ruby to scope-based resolution (RFC #909 Ring 3) (#1831)
* feat(ruby): migrate Ruby to scope-based resolution (RFC #909 Ring 3)

Implement the full scope-resolution pipeline for Ruby following the
PR #1639 (Rust migration) standard, targeting registration in
MIGRATED_LANGUAGES with 100% scope parity.

Scope resolver hooks (languages/ruby/):
- query.ts: RUBY_SCOPE_QUERY covering scopes, declarations, imports,
  type-bindings (constructor inference via .new), and references
- captures.ts: emitRubyScopeCaptures orchestrator with import
  decomposition, receiver-binding synthesis, method reclassification,
  and arity metadata for both declarations and calls
- receiver-binding.ts: self type-binding synthesis for instance methods,
  singleton methods, and class << self blocks
- interpret.ts: interpretRubyImport (wildcard semantics) and
  interpretRubyTypeBinding (YARD, constructor, alias sources)
- import-target.ts: resolveRubyImportTarget adapting the existing
  suffix resolver for require/require_relative/load
- merge-bindings.ts: tier-based shadowing (local > namespace > import)
- arity.ts: Ruby arity check with *args/**kwargs/&block support
- scope-resolver.ts: rubyScopeResolver with custom buildRubyMro
  (kind-aware IMPLEMENTS partitioning: prepend > direct > include;
  extend excluded from instance MRO per legacy semantics)
- simple-hooks.ts: bindingScopeFor, importOwningScope, receiverBinding

Wiring:
- ruby.ts provider gains 7 scope-resolution hooks
- Registered in SCOPE_RESOLVERS map and MIGRATED_LANGUAGES
- 127 legacy tests wired with createResolverParityIt('ruby')
- 27 new scope-specific tests in ruby-scope.test.ts

Parity: 89/127 legacy tests pass under registry-primary; 38 are
heritage/property/YARD gaps expected in V1. All 127 pass under legacy.

Closes #931

* feat(ruby): add emitHeritageEdges hook, YARD parsing, bare calls, property emission

Extend the scope-resolution pipeline with a new optional `emitHeritageEdges`
hook (ScopeResolver contract + run.ts wiring) that runs between
`preEmitInheritanceEdges` and `buildMro`. This lets languages whose heritage
declarations are syntactic method calls (Ruby include/extend/prepend) emit
IMPLEMENTS edges from the scope-resolver without touching the legacy pipeline.

Ruby scope-resolution improvements:
- Heritage: intercept include/extend/prepend in captures.ts, encode as
  special imports, emit IMPLEMENTS edges via emitHeritageEdges hook
- Properties: intercept attr_accessor/attr_reader/attr_writer, emit
  Property nodes + HAS_PROPERTY edges via the same hook
- Bare calls: add (body_statement (identifier)) capture to scope query,
  matching the legacy query pattern for zero-arity method calls
- YARD parsing: second-pass comment scanner for @param/@return/@type
  annotations with findFollowingMethod that handles body_statement nesting
- Query fixes: @declaration.trait for modules (was @declaration.module
  which normalizeNodeLabel didn't recognize), constant constructor
  bindings (SERVICE = UserService.new), call-return inference

Parity: 114/127 legacy tests pass under registry-primary (up from 89).
Remaining 13 are advanced type-inference chain resolution (compound
receiver, cross-file return-type propagation, for-in element types).

* feat(ruby): achieve 100% scope-resolution parity (127/127)

Fix all 13 remaining type-inference failures:

- Add expandsWildcardTo hook (expandRubyWildcardNames) so finalize can
  materialize individual bindings from require/require_relative wildcard
  imports, unblocking cross-file return-type propagation
- Add member-call-return type binding synthesis in captures.ts for
  assignments like `x = obj.method()` — enables compound receiver
  chaining through member call return types
- Add YARD @return support for attr_accessor/attr_reader/attr_writer
  calls, creating field-type bindings for chain resolution
- Add @declaration.property captures alongside __property__ imports so
  properties register in localDefs → model.fields → write-access
- Add constructor-return inference for methods ending with Foo.new()
- Add for-loop variable type aliasing in scope query
- Rebuild nodeLookup after emitHeritageEdges in run.ts so Property
  nodes created by the heritage hook are visible to downstream passes
- Extend compound-receiver resolver to handle compound member-call
  rawNames with () and increase max depth from 4 to 8
- Extend receiver-bound-calls Case 3b for compound rawNames

All 127 legacy Ruby tests pass under both REGISTRY_PRIMARY_RUBY=0
(legacy) and =1 (registry-primary). Ruby is now fully registered
in MIGRATED_LANGUAGES with 100% scope parity.

* test(ruby): add pipeline benchmark exercising heritage emission

Synthetic Ruby codebases at 100/250/500 files with include + extend +
prepend mixins, diamond mixin patterns (shared BaseMixin modules),
attr_accessor properties, YARD annotations, and cross-file imports.

Strict equality assertions verify exact IMPLEMENTS and HAS_PROPERTY
edge counts: 4 IMPLEMENTS per class (include x2, extend, prepend)
plus 1 per non-base mixin module, 3 HAS_PROPERTY per class.

Dedup in emitRubyMixinEdges prevents double-counting when the worker
path (repos >= 15 files) already created Property/IMPLEMENTS edges
before scope-resolution runs.

Scaling: 0.76x and 1.40x (both linear, well under 3x threshold).

* ci: retrigger build

* fix(ci): resolve format, registry-primary-flag, and sequential-mixin test failures

- Run prettier on all changed files (captures.ts, run.ts, ruby-scope.test.ts,
  ruby.test.ts, ruby-pipeline-benchmark.test.ts)
- Update registry-primary-flag.test.ts: use Swift (not in MIGRATED_LANGUAGES)
  instead of Ruby for the isolation and env-var mutation tests
- Pin ruby-sequential-mixin.test.ts to REGISTRY_PRIMARY_RUBY=0 (legacy mode)
  since it tests inferImplicitReceiver + selectDispatch hooks that live in the
  legacy call-processor (gated off under registry-primary)

---------

Co-authored-by: Test <test@example.com>
2026-05-26 16:16:49 +01:00
Gergő MagyarandTest d5b2edddc4 fix(test): use retry cleanup in antigravity e2e to prevent ENOTEMPTY flake (#1838)
* fix(test): use retry cleanup in antigravity e2e to prevent ENOTEMPTY flake

Replace bare `fsp.rm` / `fs.rmSync` in antigravity-hook-e2e.test.ts
afterAll with `cleanupTempDir` / `cleanupTempDirSync` from test-db.ts
which retry with backoff on transient filesystem errors.

Also make `shouldSwallowCleanupError` swallow ENOTEMPTY on all
platforms (was Windows-only). The CI failure on macOS was ENOTEMPTY
on a deeply nested node-gyp cache directory inside the temp HOME —
a cleanup-time race that retries usually resolve, but the final
attempt must not crash the test suite if the race persists.

* fix: restore fsp import needed for mkdtemp/mkdir

---------

Co-authored-by: Test <test@example.com>
2026-05-26 15:40:08 +01:00
4870879b21 fix(wiki): add budget-aware grouping to prevent context overflow on large repos (#627) (#1832)
* fix(wiki): add budget-aware grouping to prevent context overflow on large repos (#627)

When the grouping prompt exceeds 100k tokens (e.g. Apache TVM with ~2,378
files and ~306k estimated tokens), batch files by top-level directory and
issue one LLM call per batch. Partial results are deterministically merged;
any batch failure falls back to directory-based grouping.

* fix(wiki): address review findings — exact assertions, progress fix, error logging

- Replace bounds-only .toBeGreaterThan assertions with exact .toBe values
- Add per-batch budget compliance assertion for sub-batch case
- Add assertion that partial LLM results don't leak through nuclear fallback
- Pass fixedPercent/percentRange to streamOpts in batched LLM calls
- Log batch failure in onProgress before falling back to directory grouping
- Strengthen mergeGroupings dedup test from .toContain to exact .toEqual

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

* fix(wiki): prevent slug collisions and handle single-file oversize in batched grouping

mergeGroupings now normalizes module keys by slug so case/punctuation
variants ("API Routes" vs "API routes") merge into one module instead
of producing colliding .md files.

batchFilesForGrouping now truncates per-file symbol lists via binary
search when a single file exceeds GROUPING_TOKEN_BUDGET, so every
LLM request stays within the context window.

* style(wiki): apply prettier formatting to generator.ts

---------

Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-26 12:32:07 +01:00
c916c88361 feat(mcp): add limit/offset/summaryOnly pagination to impact tool (#1818)
* feat(mcp): add limit/offset/summaryOnly pagination to impact tool (#414)

The impact tool returns unbounded byDepth arrays for hub symbols (base
error classes, shared utilities), producing 140KB+ responses that get
truncated by MCP clients. maxDepth alone does not help when most
dependents are at depth 1.

Add three new parameters:
- summaryOnly: returns counts/risk/processes/modules without byDepth
- limit: caps symbols per depth level (default 100)
- offset: skips symbols for pagination

Also adds byDepthCounts to all responses so agents can see total counts
even when the symbol list is paginated or omitted.

Closes #414

* fix(mcp): prevent pagination from silently truncating cross-repo impact

Address review findings on #1818:

- F1 (blocker): _runImpactBFS no longer defaults to limit 100 when
  limit is not set — only _impactImpl (MCP entry) applies the default.
  Internal callers (impactByUid, group impact) get complete results.
  GroupToolPort.impact interface gains optional limit param, and
  cross-impact.ts passes limit: 10000 for local UID collection.

- F2 (blocker): tool description updated — byDepth is now documented
  as paginated, not 'all affected symbols'.

- F3: impactByUid calls _runImpactBFS without limit, so Phase-2
  neighbor results are no longer capped at 100.

- F4: pagination metadata now appears when offset > 0 (head truncation),
  not just tail truncation. Pagination.limit is null when uncapped.

- F5: limit/offset schema types changed from number to integer;
  Math.trunc applied in implementation as defense-in-depth.

- F6: 7 new tests — multi-depth pagination, offset-only truncation,
  offset past end, float inputs, _runImpactBFS internal uncapped path,
  collectImpactSymbolUids with paginated vs complete data.

* fix(mcp): NaN guard on pagination params, complete GroupToolPort interface

- Add Number.isFinite guard to limit/offset in _runImpactBFS so NaN
  inputs fall through to uncapped/zero defaults instead of producing
  silent empty byDepth with no truncation signal.

- Add offset and summaryOnly to GroupToolPort.impact interface to
  match the implementation and prevent silent param loss at the
  port boundary.

- Replace bounds-only toBeLessThan assertion with exact byDepthCounts
  and pagination assertions per DoD §2.7.

* fix(mcp): address remaining review findings for impact pagination

- #3: Forward limit/offset/summaryOnly through callToolAtGroupRepo
  so group-mode MCP callers can use the new pagination params.

- #4: Extract GROUP_LOCAL_PHASE_LIMIT constant from magic 10000 in
  cross-impact.ts with a comment explaining the intent.

- #7: eval-server formatImpactResult uses byDepthCounts[depth] for
  the 'and N more' suffix instead of paginated slice length.

- #8: Extract ImpactParams interface from duplicate inline type
  definitions in impact() and _impactImpl().

- #9: Add --limit, --offset, --summary-only CLI flags to the impact
  command with i18n help strings (en + zh-CN).

- #10: Clarify in tool description that limit/offset apply per depth
  level, not per total result set.

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

* @
fix(mcp): address Copilot review feedback on impact pagination

- Sanitize limit/offset with Number.isFinite in _impactImpl to prevent
  NaN passthrough from bypassing the default limit of 100
- Omit pagination.limit field instead of emitting null when paginationLimit
  is Infinity, keeping the response schema consistent
- Move GROUP_LOCAL_PHASE_LIMIT after all imports in cross-impact.ts
- Stop forwarding limit/offset/summaryOnly to group-mode impact since
  runGroupImpact overrides limit with GROUP_LOCAL_PHASE_LIMIT for UID
  collection and does not re-paginate
- Validate CLI parseInt results with Number.isFinite before passing to
  the backend, falling back to undefined so defaults apply
- Use byDepthCounts to decide whether to render depth sections in
  formatImpactResult, handling empty pages from offset past end
@

* @
fix(mcp): address code review findings on impact pagination

- Fix formatImpactResult "N more" count: use Math.min(items.length, 12)
  instead of hardcoded 12, so paginated pages with <12 items show the
  correct remaining count
- Detect summaryOnly responses (byDepth absent, byDepthCounts present)
  and show a summary-mode message instead of misleading "(0 items on
  this page — adjust offset)" per depth level
- Document that limit/offset/summaryOnly are single-repo only and
  ignored in group mode (@groupName) in MCP tool schema descriptions
- List byDepthCounts in summaryOnly description and note byDepth
  absence when summaryOnly is true
- Remove unused limit/offset/summaryOnly from GroupToolPort.impact
  interface since they are never forwarded to group impact
- Deduplicate parseInt calls in CLI tool.ts: extract to local variables
  with consistent optional-chain usage
@

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

* @
fix(group): restore limit in GroupToolPort.impact interface

cross-impact.ts passes limit: GROUP_LOCAL_PHASE_LIMIT through the
GroupToolPort.impact interface for UID collection. Only offset and
summaryOnly were truly unused — limit must stay.
@

* @
docs: add limit/offset/summaryOnly to impact tool options in README
@

---------

Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-26 08:48:24 +01:00
SparshandGergő Magyar 966ddb981e feat(cpp): thread base-specifier qualifier through dependent-base lookup (#1815) (#1819)
* feat(cpp): thread base-specifier qualifier through dependent-base lookup (#1815)

captures.ts: add extractBaseLookupQualifier, fix isBaseDependent
for qualified_identifier bases. two-phase-lookup.ts: qualifier
storage, markCppDependentBase accepts qualifier, dedup index
by nodeId (last-wins), V3 qualifier targeting (dormant).

Infrastructure delivered: qualifier extraction, storage, dedup,
isBaseDependent fix. V3 targeting dormant until qualifiedName
computation fix reaches localDefs.

Part of #1564. Infrastructure for #1815.

* fix(cpp): three conservatism fixes for dependent-base lookup

Fix 1 — Map collision in markCppDependentBase (line 83):
Change innermost storage from Map<baseName, qualifier> to
Map<baseName, Set<qualifier>> so multiple captures of the same
dependent base name with different qualifiers don't collide.

Fix 2 — Single-candidate bypass (lines 197-206):
For qualified bases with only one candidate, verify namespace match
before accepting. Unqualified bases still accept the unique candidate.
Previously accepted regardless, creating false edges.

Fix 3 — V3→V2 fallthrough (line 221):
When a syntactic qualifier is present but no exact match is found,
suppress rather than falling through to V2 prefix-heuristic. V2 only
runs for truly unqualified bases, which is what it was designed for.

All three are conservative bug fixes — turn false positives into
suppression, not behavior changes. 250/250 tests pass both modes.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-26 08:01:55 +01:00
dependabot[bot] 05151b1079 chore(deps)(deps): bump lru-cache from 11.3.6 to 11.4.0 in /gitnexus (#1826) 2026-05-26 07:28:32 +01:00
dependabot[bot] 5458ddce77 chore(deps)(deps-dev): bump @types/node in /gitnexus (#1825) 2026-05-26 05:59:15 +01:00
dependabot[bot] 3161938730 chore(deps)(deps-dev): bump vitest from 4.1.6 to 4.1.7 in /gitnexus (#1824) 2026-05-26 05:27:57 +01:00
d4449b4ec8 fix(lbug): resolve non-ASCII paths for KuzuDB on Windows (#1811) (#1817)
* fix(lbug): resolve non-ASCII paths to 8.3 short form on Windows (#1811)

KuzuDB's native C++ layer uses ANSI file APIs (fopen) on Windows.
When the repo path contains CJK or other non-ASCII characters, the
UTF-8 bytes from Node.js are misinterpreted as the system's Active
Code Page (e.g. GBK), producing a garbled path — "Error 3: The
system cannot find the path specified."

Add `toNativeSafePath()` which converts non-ASCII paths to their
Windows 8.3 short-name form (all-ASCII) before passing them to the
native layer. Applied to both the database open path and the COPY
CSV paths. No-ops on non-Windows and on all-ASCII paths.

Closes #1811

* test(lbug): add unit + integration tests for non-ASCII path handling (#1811)

- Unit tests for toNativeSafePath: ASCII passthrough, non-Windows
  no-op, Windows short-path conversion, nonexistent-path fallback
- Integration test: full initLbug + loadGraphToLbug round-trip with
  CJK characters in the storage path — runs on all platforms
- Fix toNativeSafePath to reject cmd.exe output containing '?' chars
  (replacement for unrepresentable Unicode in the console code page)
- Register integration test in vitest lbug-db project and
  cross-platform-tests.ts matrix

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

* feat(lbug): junction fallback, tmpdir CSV staging, pool-adapter coverage (#1811)

U1+U4: toNativeSafePath now tries 8.3 short path → NTFS junction
fallback → diagnostic warning. Junctions target path.dirname(p) and
reconstruct the leaf. Handles EEXIST races. Registers cleanup on
exit/SIGTERM/SIGINT. Orphan scan on first call removes stale
junctions from prior crashes.

U2: loadGraphToLbug redirects csvDir to os.tmpdir() when
storagePath contains non-ASCII on Windows, avoiding non-ASCII
characters in COPY FROM paths entirely.

U3: All 4 createLbugDatabase call sites in pool-adapter.ts now
wrap dbPath with toNativeSafePath.

* fix(test): fix CI failures from toNativeSafePath addition (#1811)

- Fix lbug-non-ascii-path integration test: use CodeRelation (actual
  relationship table name) instead of CALLS
- Add toNativeSafePath to lbug-config.js mocks in pool-wal-recovery
  and lbug-pool-win-fts-probe tests — pool-adapter now imports it

* fix(lbug): sanitize path before cmd.exe shell expansion (CodeQL)

Reject paths containing cmd.exe metacharacters (" % | & < > ^)
before interpolating into the `for %I` short-path command.
Prevents command injection via crafted path names.

* fix(lbug): address code review findings in non-ASCII path implementation

- U1: Use process.exit(0) on Windows instead of process.kill re-raise
  (SIGTERM forcefully kills on Windows, handlers never fire)
- U2: Pass safePath to openWithLockRetry so sidecar sweep targets the
  path KuzuDB actually opened, not the original non-ASCII path
- U3: Skip junction creation in worker threads (isMainThread guard) to
  prevent junction leaks from pool-adapter workers
- U4: Replace existsSync with lstatSync in orphan scan to avoid 30s
  blocking on unreachable UNC network targets

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

* fix(lbug): correct SIGTERM exit code and run Prettier (#1811)

- Use exit code 143 (SIGTERM) / 130 (SIGINT) on Windows instead of 0
  so termination is not masked as success
- Run Prettier to fix formatting (CI Gate blocker)

* fix(lbug): eliminate CodeQL command-injection taint in tryShortPath

Pass the path via GITNEXUS_SP environment variable instead of
interpolating it into the cmd.exe command string. The FOR loop
reads %GITNEXUS_SP% from the environment, so the command text is
entirely static — no user-controlled data in the shell command.

Also removes CMD_UNSAFE_RE since the env var approach makes
character-level sanitization unnecessary.

---------

Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 21:28:12 +01:00
Gergő MagyarandTest a229e8e77b fix(build): skip build.js when running outside the monorepo (#1795) (#1816)
`scripts/build.js` assumes the monorepo sibling `gitnexus-shared`
exists. When a user runs `npm install` from within a global install
directory, the `prepare` lifecycle fires `build.js`, which calls
`execSync(tsc, { cwd: nonExistentPath })` — Node reports this as
the misleading `spawnSync /bin/sh ENOENT`.

Add an early guard: if `gitnexus-shared` is absent and `dist/`
already exists (published package context), exit cleanly. If neither
exists, print a helpful error pointing to the monorepo checkout.

Co-authored-by: Test <test@example.com>
2026-05-25 15:31:11 +01:00
50c6acb108 feat(setup): implement antigravity integration setup and hook adapter… (#1730)
* feat(setup): implement antigravity integration setup and hook adapter for gitnexus

* docs(readme): list Antigravity in supported editors

* test(setup-antigravity): pin platform per-test to fix Windows CI failure

The MCP entry assertion expected `npx` directly, but on Windows
`getMcpEntry()` wraps it as `cmd /c npx ...`, which broke the Windows
runner. Pin platform to darwin in beforeEach so the existing assertion
is deterministic, restore the descriptor in afterEach, and add a
parity test for the win32 cmd-wrapper shape.

* fix(antigravity): align hook adapter to Gemini CLI schema + fix Windows CI

Rebase the Antigravity integration on the canonical Gemini CLI hooks
contract (https://geminicli.com/docs/hooks/reference/), which is the
documented schema Antigravity 2.0 inherits:

- Hook adapter: replace PreToolUse/PostToolUse with the single AfterTool
  event. BeforeTool has no documented context-injection channel in the
  Gemini contract, so augmentation runs in AfterTool where
  hookSpecificOutput.additionalContext is the documented way to append
  text to the tool result the agent reads. Stale-index hints land in the
  same channel (so the agent sees them) and are mirrored to stderr for
  terminal users. Tool-name matcher updated to Gemini CLI snake_case
  (search_file_content|glob|run_shell_command).
- Setup: write hooks to ~/.gemini/settings.json under canonical
  hooks.AfterTool[] (replaces the ad-hoc hooks.json top-level group).
  Polite-neighbor merge preserves existing user hooks. Also copy
  win-rm-list-json.ps1 alongside hook-db-lock-probe.cjs so the Windows
  MCP server ownership probe doesn't silently fail open.
- Tests: 17 regression tests covering MCP write, win32 shape, hook
  schema, polite-neighbor merge, idempotency, adapter context emission,
  stale-index hint, and skill layout.
- README: footnote documenting the AfterTool design choice and a link
  to the Gemini CLI hooks reference.

Windows CI fix: installSkillsTo previously used glob('*.md') +
glob('*/SKILL.md'), which returned zero matches under the Windows
runner's temp paths (8.3 short-name like RUNNER~1). Replace with
fs.readdir + dirent type checks — same behavior, no path quirks. This
fixes the only failing Windows job on the PR.

* fix(antigravity): address PR review — windowsHide, stale docs, dead code

Addresses the production-readiness review findings on PR #1730:

- F1 (blocker): add windowsHide:true to all four spawnSync sites in the
  Antigravity hook adapter (findCanonicalRepoRoot, runGitNexusCli's two
  branches, buildStaleIndexHint) so they don't flash console windows on
  Windows. Matches the fix #1794 already on main for the Claude hook.
- F2 (blocker): update gitnexus/README.md editor table to say AfterTool
  and link the Gemini CLI hooks reference. The published README had
  drifted to the pre-c1872b4 PreToolUse + PostToolUse schema.
- F3: rewrite the stale ~/.gemini block comment in setup.ts. It still
  described the old hooks.json + gitnexus group + grep_search design.
- F4: remove grep_search dead code from extractPattern and its doc
  comment. The registered matcher is search_file_content|glob|run_shell_command,
  so grep_search would never be invoked.
- F5: annotate timeout:10000 with a ms-unit comment noting Gemini CLI
  uses milliseconds (Claude Code uses seconds).
- F6: add the GITNEXUS_DEBUG branch to extractAugmentContext for parity
  with the Claude adapter, so suppressed augment stderr is recoverable.
- F7: stageAdapter test helper now copies win-rm-list-json.ps1 alongside
  the .cjs helpers, so the adapter's Windows lock-probe path isn't a
  silent fail-open in child-process smoke tests.

* test(antigravity): add integration tests and register in cross-platform matrix

Adds end-to-end coverage on top of the unit-level tests, per maintainer
request:

- test/integration/setup-antigravity.test.ts (10 tests): exercises the
  real setupCommand() against a temp HOME with ~/.gemini/antigravity/
  present. Verifies mcp_config.json shape, ~/.gemini/settings.json
  AfterTool entry, adapter + helpers + win-rm-list-json.ps1 copy,
  baked-in cliPath rewrite (issue #108 regression class), skill layout,
  polite-neighbor merge against existing user hooks, idempotency,
  skip-when-absent, corrupt-file safety, and key preservation.
- test/integration/antigravity-hook-e2e.test.ts (19 tests): runs the
  full install-then-execute flow — invokes setupCommand to lay down
  the adapter + helpers, then spawns the INSTALLED adapter as a real
  child process against a temp git repo + .gitnexus/. The source
  adapter cannot be spawned directly (it requires sibling .cjs helpers
  that only live in hooks/claude/); install-then-spawn mirrors the
  production codepath. Covers staleness detection across all five git
  mutation types, --embeddings propagation, polite skip on
  toolResponse.error / exit_code !== 0, augment crash-free behavior,
  cwd validation, corrupted/missing meta.json, unknown event names,
  empty stdin, and the no-.gitnexus deep-nested case.
- scripts/cross-platform-tests.ts: registers all three antigravity
  test files (unit in PLATFORM_LOGIC, two integration files in
  SPAWN_CLI) so Windows and macOS CI exercise them on every run.

* fix(antigravity): review fixes — dedup, silent-failure guard, type coercion, glob filter

- Delete mergeGeminiSettingsHooks (verbatim copy of mergeHooksJsonc),
  replace call site with the original
- Unify geminiHasGitnexusHook into hasGitnexusHook with commandFragment
  parameter; delete the duplicate
- Guard against silent adapter-copy failure: verify the adapter file
  exists before registering the AfterTool hook entry in settings.json;
  surface helper copy errors instead of swallowing
- Fix toolSucceeded type coercion: use Number() so string exit_code
  values from Gemini CLI are handled correctly
- Align glob tool extractPattern with Claude adapter's restrictive
  regex filter (/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/)
- Remove bounds-only toBeGreaterThan(0) assertion (DoD §2.7)
- Add antigravity adapter to HOOK_FILES windowsHide regression list

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

* chore: trigger CI

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 14:46:17 +01:00
baf57bec88 feat(rust): Migrate Rust to scope-based resolution (RFC #909 Ring 3) (#1639)
* Initial plan

* feat: add Rust scope-resolution hooks (RFC #909 Ring 3)

Implement the scope-based resolution pipeline for Rust, following the
established pattern from Go and other migrated languages.

New files in gitnexus/src/core/ingestion/languages/rust/:
- query.ts: tree-sitter scope query covering scopes, declarations,
  imports, type bindings, and references
- cache-stats.ts: parse cache hit/miss counters
- import-decomposer.ts: decomposes use declarations into individual
  import captures (handles grouped, wildcard, renamed, re-exported)
- receiver-binding.ts: synthesizes self type bindings for impl methods
- interpret.ts: interprets captures into ParsedImport/ParsedTypeBinding
- arity.ts: arity compatibility checker (no overloading in Rust)
- merge-bindings.ts: local-shadows-import binding merge strategy
- simple-hooks.ts: binding scope, import owning scope, receiver binding
- import-target.ts: resolves Rust module paths (crate/super/self)
- method-owners.ts: bridges impl block methods to struct defs
- captures.ts: main emit function with import decomposition and
  self-binding synthesis
- scope-resolver.ts: ScopeResolver implementation
- index.ts: barrel re-exports

Wiring changes:
- rust.ts: add scope hook imports and properties to defineLanguage
- registry.ts: register rustScopeResolver in SCOPE_RESOLVERS
- registry-primary-flag.ts: add Rust to MIGRATED_LANGUAGES

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(LANG-rust): add scope-resolution hooks and register Rust ScopeResolver

Implements RFC #909 Ring 3 deliverables:
- query.ts: tree-sitter scope query for Rust
- captures.ts: emitRustScopeCaptures with method reclassification
- import-decomposer.ts: use statement decomposition (groups, renames, globs)
- interpret.ts: interpretRustImport + interpretRustTypeBinding
- import-target.ts: crate/module/super/self path resolution
- receiver-binding.ts: self/&self/&mut self receiver synthesis
- method-owners.ts: impl block → struct ownership bridging
- arity.ts: no-overloading arity check
- merge-bindings.ts: local > import > wildcard binding precedence
- simple-hooks.ts: binding/import scope, receiver binding
- scope-resolver.ts: ScopeResolver contract implementation
- Wired into rustProvider (rust.ts) with scope hooks
- Registered in SCOPE_RESOLVERS (pipeline/registry.ts)
- NOT yet added to MIGRATED_LANGUAGES (29 advanced pattern tests pending)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8f14b730-79d4-4356-9505-325750d71f84

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* Add Rust scope-resolution integration tests (RFC #909 Ring 3)

Tests cover the core deliverables for the Rust scope-resolution pipeline:
- impl blocks and trait implementations
- Module resolution (crate::, super::, self::)
- Struct fields and type bindings
- Self/&self/&mut self receiver binding
- Generic functions (V1 ignores generic args)
- Grouped imports (use foo::{A, B})
- Renamed imports (use foo::Bar as Baz)
- Arity checking (no overloading)
- Struct literal constructor inference
- Return type inference
- Scoped/qualified calls (Foo::new())
- Enum declarations
- Multiple impl blocks
- Free function calls
- Typed let bindings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test(LANG-rust): add 28 scope-resolution integration tests

Covers 15 test suites validating: impl blocks, trait impls, grouped
imports, renamed imports, module resolution, receiver binding,
arity filtering, struct literal inference, return type inference,
qualified calls, struct fields, enums, multiple impl blocks, free
calls, and typed let bindings.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8f14b730-79d4-4356-9505-325750d71f84

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(LANG-rust): add implicit crate path fallback, 35 scope tests

- Import resolver now falls back to crate-relative for unqualified
  module paths (Rust 2015 edition compat)
- Added 7 more test cases: re-exports, shadowing, closures, default
  trait methods (35 total, exceeding ≥30 requirement)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/8f14b730-79d4-4356-9505-325750d71f84

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(rust): add Rust to MIGRATED_LANGUAGES with 100% scope-resolution parity

35/35 integration tests pass under both REGISTRY_PRIMARY_RUST=0 (legacy)
and =1 (scope-resolution). Rust scope-resolution is now the default
production call-resolution path.

* test(rust): add pipeline benchmark matching PHP benchmark pattern

Generates synthetic Rust codebases at 100/250/500 files with structs,
impl blocks, traits, cross-module use declarations, and method calls.
Measures wall-clock time, peak heap, and scaling ratios.

Results: sub-linear scaling (0.79x ratio), 500 files in 3.8s with
workers, 110MB peak heap. Gated by GITNEXUS_BENCH=1.

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

* perf(rust): optimize captures, type normalization, and method-owner linking

- Cache findEnclosingImpl result to avoid duplicate tree walk per function
- Avoid double namedChild accessor call in struct field arity counting
- Extract regex constants (REF_PREFIX_RE, PTR_PREFIX_RE) from hot normalization loops
- Replace O(s) suffix-match scan with O(1) Map lookup in method-owner linking

Benchmark: 500 files 3806ms → 2970ms (-22%), 250 files 2432ms → 1752ms (-28%)

* fix(rust): resolve CodeQL alerts — file-system race and dead code

- Remove existsSync+appendFileSync/writeFileSync TOCTOU in benchmark
  fixture generator; appendFileSync creates if missing
- Remove always-true guard on computeRustCallArity return; narrow
  return type from number|undefined to number
- Remove no-op .filter() in fixture generator

* feat(rust): hoist impl return-type bindings to struct scope for chain resolution

Synthesize a module-level duplicate of @type-binding.return captures for
methods inside impl blocks. The scope-extractor's auto-hoist places these
on the struct's Class scope, making them visible to the compound receiver
chain resolver via classScopeByDefId. Without this, method return types
are only on the impl block scope (which has no class-like def and is not
indexed by classScopeByDefId), so chains like svc.get_user().save()
cannot follow the intermediate return type.

This is the structural prerequisite for chain resolution, pattern binding,
and for-loop element-type parity (28 remaining tests). The cross-file
return-type propagation step still needs wiring for full parity.

* fix(rust): use implNode anchor for return-type hoisting — fixes chain resolution

Use the enclosing impl_item node (not tree.rootNode) as the synthetic
capture anchor. The scope-extractor's auto-hoist places bindings whose
anchor matches the innermost scope on the parent scope. With implNode,
the binding lands on the Module scope (parent of impl's Class scope),
giving declaredAtScope the correct context for findClassBindingInScope
to resolve the return type across the scope chain.

Unlocks: chain calls (svc.get_user().save()), return-type inference,
assignment chains, cross-file binding propagation, call-result binding,
deep field chains — 28→27 failing tests.

* feat(rust): add populateRangeBindings hook + .await query capture

Implement populateRustRangeBindings (Phase 2 hook, same pattern as Go's
populateGoRangeBindings) to populate type bindings that need runtime type
lookup — for-loop element types, if-let/while-let captured patterns,
match arm patterns, and struct destructuring field types.

Also add tree-sitter query capture for let x = fn().await — unwraps
await_expression to find the inner call_expression.

28→19 failing tests: fixes for-loop Tier 1c, .iter()/.into_iter(),
async .await, if-let captured_pattern.

* fix(rust): fix tuple_struct_pattern variable extraction + Result<T,E> raw type lookup

- Skip wrapper type identifier when finding bound variable in
  tuple_struct_pattern (Some(user) was binding 'Some' not 'user')
- Add lookupRawParameterType to read unstripped generic type from AST
  for Ok/Err pattern resolution (normalizeRustTypeName strips generics)

28→15 failing tests: fixes if-let Some, if-let Ok/Err, match arm patterns.

* fix(rust): fix match_arm parent traversal + raw return type for for-loop calls

- Walk up from match_arm through match_block to find match_expression
  for source variable extraction
- Add lookupRawFunctionReturnType to find unstripped return type from
  AST for same-file for-loop call expression iterables

28→14 failing tests.

* feat(rust): inject field type bindings on struct scopes for chain resolution

Walk struct_item AST nodes and inject field types (e.g., address -> Address)
as typeBindings on the struct's Class scope. The compound receiver chain
resolver uses these to follow field chains like user.address.save().

Also fixes: match_arm parent traversal to match_expression, lookupFieldType
to check typeBindings first.

28→11 failing tests: fixes field type chains, deep chains, struct destructuring.

* feat(rust): cross-file return type lookup for for-loop call iterables

Build allReturnTypes map across all parsedFiles in Phase 2 first pass,
then use it to resolve for-loop iterables like `for x in get_fn()` when
get_fn is defined in another file.

28→9 failing tests.

* feat(rust): cross-file field type map for struct destructuring

Build allFieldTypes map across parsedFiles in Phase 2 first pass. Used
by processStructDestructuring to resolve `let Point { x, y } = p` when
Point is defined in another file.

28→7 failing tests.

* fix(rust): compound assignment write capture + pending assignment fixpoint

- Add compound_assignment_expr query for +=, -=, etc. field writes
- Add processPendingAssignments with 3-pass fixpoint for field access
  and method call result variable bindings (let addr = user.address,
  let city = addr.get_city())

28→5 failing tests.

* fix(rust): identity method return-type bindings for unwrap/expect chains

Inject unwrap/expect/clone/as_ref/as_mut as return-type bindings on
struct scopes that return the struct's own type. Since normalizeRustTypeName
already unwraps Option<T> → T, calling .unwrap() on a value typed as T
is semantically an identity — the return type equals the receiver type.

28→3 failing tests: fixes user.unwrap().save() and repo.unwrap().save() chains.

* fix(rust): skip enum variant call-return bindings + cross-file pending assignments + identity alias

- Skip Some/None/Ok/Err in @type-binding.call-return — these are enum
  variant constructors, not type names; let the annotation capture win
- Add identifier alias handler in processPendingAssignments for
  `let alias = opt` chains
- Cross-file field type and method return type lookup in pending
  assignment fixpoint via findFieldTypeAcrossFiles/findMethodReturnTypeAcrossFiles
- Identity method bindings (unwrap/expect/clone) on struct scopes

155/156 tests pass (99.4%). Remaining: trait default method dispatch
via MRO (repo.count() where count has default impl on Repository trait).

* feat(rust): 100% scope-resolution parity — MRO with same-file IMPLEMENTS + trait default method reclassification

- Add buildRustMro that includes same-file IMPLEMENTS edges in the MRO
  chain, so trait default methods (e.g., repo.count()) resolve through
  the struct → trait ancestry walk
- Only add IMPLEMENTS to MRO when struct and trait are in the same file;
  cross-file trait calls require the trait to be imported (Rust semantics)
- Reclassify function_item inside trait_item as @declaration.method so
  default trait methods register in the model's methods lookup

156/156 legacy parity tests pass. 35/35 scope tests pass. 0 regressions.

* fix(rust): address code review findings — null guard, name collision, scope order

- Fix Array.find() null guard: check === undefined not === null in
  processCapturedPattern (find() never returns null)
- Fix allReturnTypes/allFieldTypes name collision: delete entry on
  second occurrence so colliding names (new, default, Config) produce
  no result rather than a wrong result
- Fix lookupTypeInScopes: search function scope then module scope only,
  skip unrelated Class scopes that could shadow names from other functions

---------

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: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 13:20:08 +01:00
5ce448a93a feat(wiki): support local Claude and Codex providers (#1769)
* feat(wiki): support local Claude and Codex providers

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

* fix(wiki): address local CLI provider review findings

- Add subprocess timeout: LocalCLIConfig gains requestTimeoutMs,
  runLocalCLI sets a kill timer that rejects with an actionable error
  matching the HTTP timeout message format. --timeout is no longer
  silently ignored for claude/codex providers.
- Add windowsHide: true to spawn() to prevent console window flash on
  Windows, matching cursor-client.ts behavior.
- Skip GITNEXUS_MODEL env var for local providers so a user's OpenAI
  model name doesn't cross-contaminate claude/codex CLI invocations.
  Precedence for local providers: --model → savedLocalModel → ''.
- Guard against empty stdout: reject with actionable error when CLI
  exits 0 but produces no output, preventing silent empty wiki pages.

* fix(wiki): address deep-review findings in local CLI providers

- Move empty-output guard from runLocalCLI to per-provider callers so
  Codex can read --output-last-message file even when stdout is empty
- Merge existing config in interactive setup (local + Azure paths) to
  prevent saveCLIConfig from erasing previously saved API keys
- Use StringDecoder for stdout/stderr to handle multi-byte UTF-8 chars
  split across pipe chunk boundaries
- Distinguish ENOENT from non-zero exit in detectLocalCLI so users see
  auth guidance instead of misleading "CLI not found" when the binary
  exists but is not authenticated

* test(wiki): add subprocess contract tests for local CLI providers

Add 21 integration-level tests covering the Claude and Codex subprocess
contracts that wiki-flags.test.ts mocks out:

- Claude argv: -p, --output-format text, --no-session-persistence,
  --model conditional, stdin prompt content, CI=1, windowsHide:true
- Codex argv: exec subcommand, --sandbox read-only, -c approval_policy,
  --output-last-message temp path, --cd, stdin marker, --model
- Timeout: kill timer fires and rejects, no timer when unset
- Codex file fallback: stdout used when file missing, error when both empty
- detectLocalCLI: warn on non-ENOENT, silent on ENOENT
- onChunk: cumulative byte count forwarded

Also register the test in cross-platform-tests.ts SPAWN_CLI section and
fix detectLocalCLI ENOENT detection logic (invert the check so non-ENOENT
errors produce a warning).

* fix(wiki): platform-aware process tree kill and Codex contract snapshot

- Add killChildTree helper that uses taskkill /T /F /PID on Windows to
  terminate the entire process tree (including cmd.exe grandchildren),
  with fallback to child.kill() if taskkill fails or on non-Windows
- Add Codex CLI flag contract snapshot test that locks the exact spawn
  args — any flag rename, reorder, or removal is caught immediately
- Add Windows taskkill tests: success path asserts taskkill called with
  correct PID and /T /F flags, failure path verifies child.kill() fallback

---------

Co-authored-by: eddie.pan2 <eddie.pan2@jtexpress.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
2026-05-25 12:59:48 +01:00
SparshandGergő Magyar 1bbc876336 fix(cpp): dependent-base resolution across nested/inline namespaces (#1634) (#1814)
* fix(cpp): dependent-base resolution across nested/inline namespaces (#1634)

Replace exact namespace-prefix match with prefix-contains filter capped
at one level deeper, then accept only if exactly one candidate survives.

Behavior change:
- Derived<T> in ns::outer can now find Inner<T> in ns::outer::inner
  (nested namespace) or ns::v1 (inline namespace) via prefix walking
- Global-scope deriving classes match any single-segment namespace
- Sibling namespace collisions (e.g. detail::Inner vs public_api::Inner)
  correctly suppress when multiple candidates share the same simple name
- Deep nesting (ns → ns.a.b) still suppresses (one-level cap)

Fixtures added:
  pos:  nested ns, this->f() -> 1 edge to inner::Inner::f
  neg:  no Inner exists -> 0 edges
  inline:  inline namespace variant -> 1 edge
  sibling-suppress:  sibling collision -> 0 edges (ambiguity suppressed)

Part of #1564.
64.

* test: add deep-nesting suppression fixture, link #1815 in comment, unqualify inline fixture

- Update code comment to reference follow-up issue #1815 instead of
  'deferred to follow-up'
- Inline fixture: drop explicit v1:: qualifier (exercise inline-expansion
  path more idiomatically as DoD intended)
- Add deep-nesting suppression fixture (ns.a.b -> 0 edges) that pins the
  one-level cap as a documented invariant
- Add legacy parity entry for deep-nesting fixture

Part of #1564, #1634.

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-25 12:36:06 +01:00
Gergő MagyarandTest 5e8690f992 feat(progress): add per-language progress reporting to scope-resolution phase (#1813)
* feat(progress): add per-language progress reporting to scope-resolution phase (#1741)

The scope-resolution phase (which can run 74+ minutes on large Java/Kotlin
repos) previously emitted zero progress updates, causing the CLI progress bar
to freeze at ~49% with a stale "Parsing code" label — making users think
the tool was stuck.

- Add `scopeResolution` to PipelinePhase type and PHASE_LABELS
- Add `onProgress` callback to `runScopeResolution` with per-file updates
  during the extract loop and sub-phase boundary markers (building scope
  model, resolving references, emitting edges)
- Wire progress through `scopeResolutionPhase` with pre-counted file totals,
  per-language labels, and pipeline-wide percent mapping (90-95 internal)
- Bump mro/communities/processes percent ranges to 95-100 to maintain
  monotonic progress after scope resolution
- Add `scopeResolution` to mro's deps (latent ordering fix: mro reads
  EXTENDS edges that scope resolution writes via preEmitInheritanceEdges)

* fix(progress): clamp overallRatio, fire final extract event, fix mro @deps JSDoc

- Clamp overallRatio to [0,1] so percent never exceeds 95 when
  readFileContents drops files (langFileCount < totalScopeFiles)
- Fire onProgress for the last file in the extract loop even when
  files.length is not divisible by progressInterval
- Update mro @deps JSDoc to include scopeResolution

* fix(progress): ensure bar redraws at every state transition

- Fire initial 'extracting' event at file 0 so the sub-phase label
  appears immediately, not after progressInterval files
- Emit a completion event at percent 95 when scope resolution finishes
  so the bar definitively reaches the phase ceiling before mro starts

* feat(progress): improve UX with human-readable elapsed, language counter, cleaner labels

- Format elapsed time as "5m 12s" / "1h 20m" instead of raw "(312s)"
  for all pipeline phases (CLI-wide improvement)
- Add language counter "[1/3]" to scope-resolution detail so users
  know how many languages remain and which is active
- Rename sub-phases for clarity: "building scope model" → "analyzing
  types", "emitting edges" → "linking symbols"
- Remove nested parentheses from detail strings for cleaner display
- Expand scope-resolution percent range from 5 to 8 points (90-98
  internal → 54-59% display) for more visible bar motion
- Re-allocate mro (98), communities (98-99), processes (99-100)

* feat(progress): typed sub-phases, i18n locales, and test coverage

- Extract ScopeResolutionSubPhase union type with exhaustive switch
  guard so adding a sub-phase without updating phase.ts is a compile
  error
- Add scopeResolution key to en and zh-CN locale files so the web UI
  shows translated labels instead of raw message fallback
- Extract formatElapsed to its own module with 7 boundary-value tests
  (0s, 59s, 60s, 3599s, 3600s, 3661s, 7323s)
- Add runScopeResolution onProgress integration test proving sub-phase
  order (extracting → analyzing types → resolving references → linking
  symbols) and the 0-file early-return path

---------

Co-authored-by: Test <test@example.com>
2026-05-25 11:53:54 +01:00
efcab45560 feat(web): support GITNEXUS_BACKEND_URL env var for Docker deployments (#1286)
* feat(web): support GITNEXUS_BACKEND_URL env var for Docker deployments

* fix(docker): escape inline script injection to prevent XSS and add server-level integration tests

- Add jsonForScriptTag() that escapes <, >, & after JSON.stringify to prevent </script> breakout in inline config script
- Sanitize rawBackendUrl in warning log to prevent log injection via newlines
- Replace 5 duplicated-helper injection tests with 7 server-level HTTP integration tests that spawn the real docker-server.mjs with GITNEXUS_BACKEND_URL set
- Add XSS-specific test: URL containing </script> must produce exactly 1 <script> tag
- Add empty-string backendUrl frontend test
- Improve Docker Compose Linux guidance with explicit <server-ip> example

* fix(docker): harden log sanitization, fix error leak, fix killAndWait race

- Broaden log sanitization regex from [\r\n] to [\x00-\x1f\x7f] to strip
  all C0 control characters including ANSI escape sequences
- Replace error.message leak in 500 handler with generic string; log the
  real error server-side via console.error
- Fix killAndWait TOCTOU race by registering exit listener before kill
  and adding post-kill exitCode guard

* fix(docker): handle readFile race to resolve CodeQL file-system-race alert

Wrap readFile in try/catch so the TOCTOU between stat() and readFile()
is handled gracefully — if the file vanishes between the check and the
read, return 404 instead of crashing.

* @
fix(docker): eliminate TOCTOU race and format web components

Replace the previous try/catch approach with fs.promises.open() to
get a file handle, then use handle.stat()/readFile()/createReadStream()
from the same fd — properly eliminates the CodeQL "file system race
condition" alert by removing the window between stat() and read.

Also runs prettier on the 5 web component files that were failing
the format CI check.
@

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

* chore: trigger CI

* @
fix(docker): pass GITNEXUS_BACKEND_URL to the web container

The env var was documented but commented out, so docker-server.mjs
never received it and the config injection was dead. Uncomment
the environment block with a passthrough default so users can
set GITNEXUS_BACKEND_URL in .env or their shell for remote/custom
deployments.
@

* @
fix(docker): eliminate stat() to resolve CodeQL js/file-system-race

CodeQL pairs any stat() (FileCheck) with a subsequent open() (FileUse)
on an aliased path. The previous approach kept stat() for directory
detection, which the analyzer flagged regardless of the fd-based reads.

Replace stat() entirely with open() + handle.stat(). On Linux (Docker),
open() succeeds for directories, so handle.stat().isDirectory() detects
them without a standalone stat() call. This removes the FileCheck node
from the data-flow graph, eliminating the alert at its source.
@

* @
fix(docker): break CodeQL path alias chain between open() calls

CodeQL js/file-system-race pairs two open() calls when their path
arguments are data-flow aliased. The previous approach derived
the fallback path from the request path (resolve(initialPath,
index.html)), creating an alias chain the analyzer could trace.

Restructure so the SPA fallback uses a module-level constant
(spaFallback = resolve(root, index.html)) with zero data-flow
from the request. The two open() calls now have provably
independent path arguments, eliminating the FileCheck/FileUse pair.

Also simplifies the logic: for an SPA, all non-file requests serve
root/index.html — no directory/index.html detection needed since
the client-side router handles subroutes.
@

---------

Co-authored-by: Test <test@example.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 11:21:11 +01:00
Sparsh 73a6a5376e fix(cpp): thread call-site types into qualified member lookup (#1632) (#1810)
* fix(cpp): thread call-site types into qualified member lookup (#1632)

Widen Callsite (arity optional, add argumentTypes) and add optional
callsite?: Callsite to ScopeResolver.resolveQualifiedReceiverMember.
receiver-bound-calls.ts passes the ReferenceSite through structurally;
resolveCppQualifiedNamespaceMember forwards it to narrowOverloadCandidates
along with cppConversionRank, enabling exact-type and conversion-rank
disambiguation across inline-namespace children.

Behavior change:
- outer::foo(42) where v1 declares foo(int) and v2 declares foo(double)
  now resolves to v1::foo (was: 0 edges, conservatively suppressed).
- Same-name same-normalized-signature (e.g. foo(int) vs foo(long)) still
  suppresses at 0 edges via isOverloadAmbiguousAfterNormalization.
- ADL using-import path (resolveAdlCandidates) unchanged — passes no
  callsite, narrowing degrades to existing pass-through behavior.

Closes #1632. Part of #1564.

* fix(cpp): update legacy parity expected-failure list for #1632

- Remove stale expected-failure entry for old diff-sigs test name
  (test now expects 1 edge; legacy DAG also emits 1 edge)
- Add entry for normalized-signature ambiguity (int vs long) test
- Rename describe block from 'conservative suppress' to
  'distinct signatures resolved via call-site types'

Verified both modes:
  REGISTRY_PRIMARY_CPP=1: 241/241 passed
  REGISTRY_PRIMARY_CPP=0: 194 passed, 47 skipped, 0 failed
2026-05-25 07:11:47 +01:00
968 changed files with 82237 additions and 27910 deletions
+43
View File
@@ -0,0 +1,43 @@
# GitNexus PR Reviewer Swarm — Claude Code adapter
This is the **Claude Code** entrypoint for the cross-CLI GitNexus PR reviewer swarm. The
review logic itself is CLI-neutral and lives in **[`pr-swarm-review/`](../pr-swarm-review/README.md)**
— that README is the canonical guide and covers every CLI (Claude Code, Gemini, Copilot,
Cursor, Codex, and any AGENTS.md-aware agent).
## Invocation (Claude Code)
```
/gitnexus-pr-swarm-review <PR URL or PR number>
```
Runs in **Swarm mode**: the coordinator skill dispatches the seven `gitnexus-*` subagents in
parallel (lanes 1–2 first, 3–6 in parallel, lane 7 last as a hard gate).
## Files in this adapter
| File | Role |
|------|------|
| `.claude/skills/gitnexus-pr-swarm-review/SKILL.md` | Coordinator — runs Swarm mode per `pr-swarm-review/orchestration.md` |
| `.claude/agents/gitnexus-*.md` | Seven thin subagent wrappers; each reads its canonical persona in `pr-swarm-review/personas/` |
Each subagent keeps valid Claude Code frontmatter (model, tools, etc.); the mechanical
verifier lanes (`test-ci-verifier`, `branch-hygiene-reviewer`) run on Haiku, the analytical
lanes on Sonnet.
## Key properties
- **Read-only.** Tools limited to Read/Grep/Glob/Bash, and every persona enforces an
explicit permitted/prohibited Bash list. No agent edits files, commits, or posts.
- **Evidence-grounded**; **missing visibility becomes verification work**; **manually invoked.**
## Editing
Edit review behavior in the canonical files under `pr-swarm-review/` (orchestration +
personas), **not** in these wrappers. After adding or editing files in `.claude/agents/`,
restart Claude Code so it reloads the agent definitions.
## Relationship to `/gitnexus-pr-review`
Coexists with the single-agent `/gitnexus-pr-review` skill (a linear checklist using GitNexus
MCP tools). This swarm is the multi-persona deep production-readiness review.
@@ -0,0 +1,24 @@
---
name: gitnexus-branch-hygiene-reviewer
description: "GitNexus branch hygiene and mergeability reviewer. Use to classify merge state, conflicts, stale branches, merge-from-main commits, unrelated churn, mixed domains, and whether rebase or split is required."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-haiku-4-5-20251001
maxTurns: 30
---
# GitNexus Branch Hygiene & Mergeability Reviewer
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/02-branch-hygiene-reviewer.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
@@ -0,0 +1,24 @@
---
name: gitnexus-docs-dod-reviewer
description: "GitNexus docs and Definition-of-Done reviewer. Use to translate repo guidance, linked issues, changed domains, docs requirements, release notes, and acceptance criteria into a PR-specific DoD."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-sonnet-4-6
maxTurns: 30
---
# GitNexus Docs & Definition-of-Done Reviewer
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/06-docs-dod-reviewer.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
@@ -0,0 +1,24 @@
---
name: gitnexus-pr-facts-historian
description: "GitNexus PR facts and repository-history investigator. Use to gather PR identity, visible GitHub state, changed files, commits, linked issues, related PRs, historical fixes, regressions, stale follow-ups, and missing visibility."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-sonnet-4-6
maxTurns: 40
---
# GitNexus PR Facts & Repository-History Investigator
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/01-pr-facts-historian.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
+24
View File
@@ -0,0 +1,24 @@
---
name: gitnexus-risk-architect
description: "GitNexus production-risk reviewer. Use for risk-model-first review of changed files, runtime behavior, multi-domain changes, user impact, failure modes, compatibility, and merge-blocking risk."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-sonnet-4-6
maxTurns: 40
---
# GitNexus Production-Risk Architect
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/03-risk-architect.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
@@ -0,0 +1,24 @@
---
name: gitnexus-security-boundary-reviewer
description: "GitNexus security and trust-boundary reviewer. Use for auth, permissions, secrets, injection, unsafe parsing, external input handling, hidden Unicode, YAML/Docker/workflow risks, and suspicious non-ASCII hygiene."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-sonnet-4-6
maxTurns: 35
---
# GitNexus Security & Trust-Boundary Reviewer
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/05-security-boundary-reviewer.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
@@ -0,0 +1,24 @@
---
name: gitnexus-synthesis-critic
description: "GitNexus final review synthesis critic. Use to check whether the final PR review is evidence-grounded, risk-prioritized, GitNexus-specific, non-generic, and follows required verdict rules."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-sonnet-4-6
maxTurns: 25
---
# GitNexus Final-Review Synthesis Critic
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/07-synthesis-critic.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
@@ -0,0 +1,24 @@
---
name: gitnexus-test-ci-verifier
description: "GitNexus test and CI reviewer. Use to verify whether changed behavior is covered by targeted tests, whether CI actually runs those tests, and whether workflow changes weaken validation."
tools:
- Read
- Grep
- Glob
- Bash
model: claude-haiku-4-5-20251001
maxTurns: 35
---
# GitNexus Test & CI Verifier
Your complete operating spec — role, what to inspect, classifications, and the required output sections — lives in the canonical, CLI-neutral persona file:
**`pr-swarm-review/personas/04-test-ci-verifier.md`**
Read that file now with the Read tool and follow it exactly. It is the single source of truth shared across all AI CLIs; this subagent only adapts it to Claude Code. The orchestration contract (lane order, Swarm vs Solo execution, output structure) is in `pr-swarm-review/orchestration.md`.
## Rules (always enforced)
- **Do not edit files.** You are read-only.
- **Bash is read-only.** Permitted: `git log`, `git diff`, `git show`, `git grep`, `git ls-files`, `gh pr view`, `gh pr diff`, `gh pr checks`, `gh issue view`, and inspection tools (`grep`, `cat`, `find`, `ls`). Prohibited: any command that writes files, modifies git state (`git commit`, `git add`, `git checkout -- <path>`), posts to GitHub (`gh pr comment`, `gh pr review`, `gh issue comment`), installs packages, or runs arbitrary scripts.
@@ -0,0 +1,31 @@
---
name: gitnexus-pr-swarm-review
description: "Run a GitNexus production-readiness pull request review using a coordinated reviewer swarm."
---
# GitNexus PR Swarm Review (Claude Code adapter)
Use this skill to review a GitNexus pull request and produce a production-readiness review.
```
/gitnexus-pr-swarm-review <PR URL or PR number>
```
You are the **swarm coordinator**. The full review contract — lanes, dependencies,
classifications, output structure, finding format, hidden-Unicode checks, and behavior
rules — is the canonical, CLI-neutral spec:
**`pr-swarm-review/orchestration.md`** — read it now and follow it.
This adapter only pins the Claude Code specifics:
- **Run in Swarm mode.** Dispatch each lane as its own subagent via the Agent tool. The
seven subagents are the project agents named `gitnexus-*` (one per persona); each reads
its canonical persona under `pr-swarm-review/personas/`. Run lanes 1–2 first, lanes 3–6
in parallel after, and lane 7 last on the draft.
- **Lane 7 is a hard gate.** Do not emit the final review while the synthesis critic's
"Required corrections before posting" section is non-empty — revise and re-run it.
- Stay **read-only**: investigate and report; never edit, commit, or post.
Do not flatten the review into a generic checklist; delegate to the subagents and
synthesize per `orchestration.md`.
@@ -5,14 +5,16 @@ description: "Use when the user needs to run GitNexus CLI commands like analyze/
# GitNexus CLI Commands
All commands work via `npx` — no global install required.
Commands below use `node .gitnexus/run.cjs <command>` — the project-local runner `gitnexus analyze` drops next to the index. It auto-selects an available runner at call time (global `gitnexus`, else `pnpm dlx`, else `npx`), so no package-manager assumption and no global install is required.
> **Not analyzed yet, or `node .gitnexus/run.cjs` reports `Cannot find module`** (the gitignored runner is absent — e.g. a fresh clone or `git clean`)? (Re)generate it with `npx gitnexus analyze` from the project root. On **npm 11.x**, if `npx` crashes during install (`node.target is null`), install once with `npm i -g gitnexus` (then `gitnexus analyze`) or use `pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze`. See [#1939](https://github.com/abhigyanpatwari/GitNexus/issues/1939).
## Commands
### analyze — Build or refresh the index
```bash
npx gitnexus analyze
node .gitnexus/run.cjs analyze
```
Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
@@ -28,7 +30,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
### status — Check index freshness
```bash
npx gitnexus status
node .gitnexus/run.cjs status
```
Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.
@@ -36,7 +38,7 @@ Shows whether the current repo has a GitNexus index, when it was last updated, a
### clean — Delete the index
```bash
npx gitnexus clean
node .gitnexus/run.cjs clean
```
Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.
@@ -49,7 +51,7 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
### wiki — Generate documentation from the graph
```bash
npx gitnexus wiki
node .gitnexus/run.cjs wiki
```
Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
@@ -66,7 +68,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
### list — Show all indexed repos
```bash
npx gitnexus list
node .gitnexus/run.cjs list
```
Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
@@ -16,23 +16,23 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@@ -40,7 +40,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
| Symptom | GitNexus Approach |
| -------------------- | ---------------------------------------------------------- |
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@@ -48,24 +48,24 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
@@ -75,11 +75,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
@@ -18,20 +18,20 @@ description: "Use when the user asks how code works, wants to understand archite
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
> If step 2 says "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@@ -47,18 +47,18 @@ description: "Use when the user asks how code works, wants to understand archite
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@@ -68,10 +68,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
@@ -15,7 +15,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
> If step 1 warns the index is stale, run `node .gitnexus/run.cjs analyze` in the terminal first.
## Skills
@@ -17,22 +17,22 @@ description: "Use when the user wants to know what will break if they change som
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@@ -55,10 +55,10 @@ description: "Use when the user wants to know what will break if they change som
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@@ -73,10 +73,10 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@@ -86,7 +86,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
@@ -18,24 +18,24 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal before reviewing.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal before reviewing.
## Checklist
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:
@@ -16,78 +16,78 @@ description: "Use when the user wants to rename, extract, split, move, or restru
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
@@ -98,24 +98,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
| ------------------- | ----------------------------------------- |
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```
@@ -0,0 +1,17 @@
# GitNexus PR Swarm Review
You are the GitNexus PR review coordinator. Review the pull request named after this command
(a PR URL or number for `https://github.com/abhigyanpatwari/GitNexus`). If none was given,
ask for one.
Read `pr-swarm-review/orchestration.md` in this repository and follow it exactly — it is the
canonical, CLI-neutral review contract (lanes, classifications, output structure, finding
format, hidden-Unicode checks, behavior rules).
Run in **Solo mode**: you are a single agent, so perform all seven lanes yourself in
dependency order, adopting each persona in `pr-swarm-review/personas/0N-*.md` in turn
(lanes 1–2 first, then 3–6, then lane 7). Keep every lane's findings in context. Lane 7
(synthesis critic) is a hard gate: do not emit the final review until its "Required
corrections before posting" section is empty.
Stay strictly read-only: investigate and report; never edit files, commit, or post to GitHub.
+153
View File
@@ -0,0 +1,153 @@
# syntax=docker/dockerfile:1
# Base image: Microsoft's TypeScript+Node devcontainer image. It works on both
# linux/amd64 and linux/arm64, gets monthly security patches, and ships the
# non-root `node` user (UID 1000, i.e. user ID 1000), zsh + Oh My Zsh, eslint
# global, and the `gh` CLI.
#
# We pin the image by digest, not by tag. That way a silent upstream retag can't
# change the build under us. This matches the Dockerfile.cli /
# gitnexus/Dockerfile.test convention and issue #1451.
#
# We pin it as a bare `name@digest` with NO `:tag` prefix on purpose. The
# production Dockerfiles use plain `docker build`, but this one is built by
# `@devcontainers/cli` / the VS Code Dev Containers resolver. That resolver's
# image-name parser rejects the combined `name:tag@sha256:...` form.
#
# The digest below is for the `1-22-bookworm` tag. It is the multi-arch
# manifest-list digest, so it still picks the right platform. To refresh it when
# bumping the readable tag, run:
# docker buildx imagetools inspect \
# mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm \
# --format '{{json .Manifest.Digest}}'
FROM mcr.microsoft.com/devcontainers/typescript-node@sha256:7c2e711a4f7b02f32d2da16192d5e05aa7c95279be4ce889cff5df316f251c1d
# Build args. We deliberately set no version defaults here. devcontainer.json
# `build.args` is the single source of truth for versions. A standalone
# `docker build .devcontainer/` (for example, a CI smoke test) must pass each
# version with --build-arg. Without a default, the build fails loudly instead of
# silently drifting from the version pinned in devcontainer.json.
ARG CLAUDE_CODE_VERSION
ARG CODEX_VERSION
# Cursor is pinned by version plus a per-arch tarball sha256 hash. The install
# step below verifies that hash. All three values live in devcontainer.json
# build.args. They follow the same rule as the others: one source of truth, and
# no default so the build fails loudly if a value is missing.
ARG CURSOR_VERSION
ARG CURSOR_SHA256_X64
ARG CURSOR_SHA256_ARM64
# Bun is installed via the official remote script (bun.sh/install), pinned by
# version. UNLIKE Cursor and the npm packages, this install path runs an
# UNVERIFIED remote script — there is no tarball-hash check. Chosen explicitly
# at request time over the pin-by-sha256 alternative for install-script
# simplicity. To harden later, switch to a pinned tarball + per-arch sha256 in
# the Cursor style (release artifacts at github.com/oven-sh/bun/releases).
ARG BUN_VERSION
ARG TZ=UTC
ARG USERNAME=node
# Copy the build-only ARGs into runtime ENV so shells and lifecycle scripts can
# read them. We deliberately do not set CLAUDE_CONFIG_DIR here. Its one true
# value lives in devcontainer.json `containerEnv`, and the runtime value wins
# anyway.
ENV CLAUDE_CODE_VERSION=${CLAUDE_CODE_VERSION} \
CODEX_VERSION=${CODEX_VERSION} \
CURSOR_VERSION=${CURSOR_VERSION} \
BUN_VERSION=${BUN_VERSION} \
BUN_INSTALL=/home/${USERNAME}/.bun \
TZ=${TZ} \
DEVCONTAINER=true \
NODE_OPTIONS=--max-old-space-size=4096 \
POWERLEVEL9K_DISABLE_GITSTATUS=true
# Native build toolchain that gitnexus/postinstall needs. It compiles
# tree-sitter native bindings, the vendored Dart/Proto/Swift grammars, and the
# @ladybugdb/core N-API addon (a native Node add-on). python3, make, and g++ are
# required. This mirrors the apt block in the existing Dockerfile.cli /
# gitnexus/Dockerfile.test images.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3 make g++ git curl ca-certificates bash unzip \
&& rm -rf /var/lib/apt/lists/*
# Create and chown the named-volume mount points (~/.npm, ~/.local,
# /commandhistory) up front. That way an empty volume inherits `node:node`
# ownership the first time it is mounted. The three CLI config dirs (~/.claude,
# ~/.codex, ~/.cursor) are bind-mounted from the host instead. A bind mount
# completely hides the image-side ownership, so those paths need no chown here.
RUN mkdir -p \
/home/${USERNAME}/.npm \
/home/${USERNAME}/.local/bin \
/commandhistory \
&& chown -R ${USERNAME}:${USERNAME} \
/home/${USERNAME}/.npm \
/home/${USERNAME}/.local \
/commandhistory
USER ${USERNAME}
# Install Claude Code and the Codex CLI globally, as the `node` user. The base
# image sets /usr/local/share/npm-global as the npm-global prefix and makes the
# `npm` group writable by `node`. So `npm install -g` works without sudo. Both
# versions come from build args. To upgrade, bump them in devcontainer.json and
# rebuild.
RUN npm install -g \
@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} \
@openai/codex@${CODEX_VERSION}
# Install the Cursor CLI. It is pinned and hash-verified, and we run no remote
# script. The cursor.com/install script just detects os/arch, downloads a
# versioned tarball from
# downloads.cursor.com/lab/<version>/<os>/<arch>/agent-cli-package.tar.gz,
# extracts it, and symlinks `agent`/`cursor-agent` into ~/.local/bin. We do that
# ourselves against a PINNED version plus a per-arch sha256 hash. So the build
# runs no unverified remote code. This matches how we pin the base image and npm
# packages by digest (issue #1451). The download is fail-closed: if the hash
# does not match, the build aborts.
#
# To bump: set CURSOR_VERSION and both CURSOR_SHA256_* in devcontainer.json
# build.args. Get each arch's hash with:
# curl -fSL https://downloads.cursor.com/lab/<ver>/linux/<x64|arm64>/agent-cli-package.tar.gz | sha256sum
#
# TARGETARCH is the per-platform build arg that BuildKit sets automatically. It
# must be (re)declared in this stage to be visible. When the build is a
# non-BuildKit `docker build`, TARGETARCH is unset, so we fall back to `dpkg
# --print-architecture`.
ARG TARGETARCH
RUN set -eux; \
arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
case "$arch" in \
amd64) cursor_arch=x64; cursor_sha="${CURSOR_SHA256_X64}";; \
arm64) cursor_arch=arm64; cursor_sha="${CURSOR_SHA256_ARM64}";; \
*) echo "unsupported architecture for Cursor: $arch" >&2; exit 1;; \
esac; \
url="https://downloads.cursor.com/lab/${CURSOR_VERSION}/linux/${cursor_arch}/agent-cli-package.tar.gz"; \
curl -fSL --retry 3 --max-time 120 -o /tmp/cursor.tgz "$url"; \
echo "${cursor_sha} /tmp/cursor.tgz" | sha256sum -c -; \
dir="/home/${USERNAME}/.local/share/cursor-agent/versions/${CURSOR_VERSION}"; \
install -d "$dir" "/home/${USERNAME}/.local/bin"; \
tar --strip-components=1 -xzf /tmp/cursor.tgz -C "$dir"; \
test -x "$dir/cursor-agent"; \
ln -sf "$dir/cursor-agent" "/home/${USERNAME}/.local/bin/agent"; \
ln -sf "$dir/cursor-agent" "/home/${USERNAME}/.local/bin/cursor-agent"; \
rm -f /tmp/cursor.tgz
# Install Bun via the official remote installer, pinned by version. The first
# positional arg to `bash` is the release tag (`bun-vX.Y.Z`), so a specific
# version is fetched even though the install script itself is downloaded fresh
# on every build. NOTE: this is the ONE remote script we run unverified in
# this image — Cursor and the base image are pinned by sha256/digest. Hardening
# path: switch to a pinned tarball + per-arch sha256 in the Cursor style
# (artifacts at github.com/oven-sh/bun/releases). `BUN_INSTALL` is set in ENV
# above so the binary lands at a known path regardless of any rc-file edits
# the installer makes (which we ignore — we own the shell rc files).
RUN set -eux; \
curl -fsSL --retry 3 --max-time 120 https://bun.sh/install \
| bash -s "bun-v${BUN_VERSION}"; \
test -x "${BUN_INSTALL}/bin/bun"
# Put ~/.local/bin and Bun's bin dir on PATH for interactive shells and
# lifecycle scripts. ~/.local/bin is where Cursor's installer drops the `agent`
# and `cursor-agent` symlinks; ${BUN_INSTALL}/bin is where the Bun installer
# drops `bun` / `bunx`.
ENV PATH=/home/${USERNAME}/.local/bin:${BUN_INSTALL}/bin:${PATH}
+364
View File
@@ -0,0 +1,364 @@
# GitNexus Devcontainer
A cross-platform Dev Container that pre-installs Claude Code, OpenAI Codex CLI, Cursor CLI, and Bun alongside the GitNexus native build chain. Supported hosts: **macOS, Linux, Windows 11 (native), and Windows 11 via WSL2.** Windows-native needs a **one-time `HOME` env var setup** — handled automatically by the `initializeCommand` on first run (see [Windows 11 setup](#windows-11-setup)).
> ### ⚠️ Read this before using it on a work machine
>
> This devcontainer **does not write to your host AI-CLI config.** Your skills, agents, commands, plugins, memory, prompts, and rules are **copied once** from a read-only host stage into a per-container volume on first create; the container edits its own copy and can never write back. So a compromised workspace dependency running in the container **cannot** drop a malicious agent, command, skill, or plugin onto your host for your next host CLI session to load — the write-through vector earlier versions had is closed. Your **credentials** (Claude/Codex/Cursor logins, plus `gh`) likewise stay in per-container volumes and are never written back, and `~/.ssh`, `~/.aws`, `~/.azure`, and `~/.docker` are mounted **read-only**.
>
> What is **still** exposed: the read-only host stages (`/host/.claude`, `/host/.codex`, `/host/.cursor`, `/host/.claude-mem`) and the read-only credential mounts are all **readable** inside the container. A compromised dependency can therefore READ your host CLI config, memory, SSH/cloud credentials, and GitHub token — and there is **no egress firewall yet**, so it has the network to exfiltrate what it reads. Read-only protects you from tampering and write-back, not from disclosure.
>
> The trade-off of the copy model: host and container config **diverge after first create.** A skill or plugin you add on the host later won't appear in the container until you wipe the config volume and rebuild (see [§ Rebuild / reset](#rebuild--reset)). Edits you make inside the container persist across rebuilds but never reach the host.
## Quick start
1. Install [Docker Desktop](https://docs.docker.com/desktop/) (Windows/macOS) or Docker Engine (Linux).
2. Install [VS Code](https://code.visualstudio.com/) with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
3. Install [Node.js](https://nodejs.org/) on the **host** (Node 18+). This is the only host-side toolchain dependency beyond Docker and VS Code — the devcontainer's `initializeCommand` runs `node .devcontainer/ensure-host-config-dirs.cjs` to set up the bind-mount source directories before container create. If you already use Claude Code or another Node-based CLI on the host, you're already set.
4. Open the repo in VS Code → Command Palette → **Dev Containers: Reopen in Container**.
5. Wait for the first build (~3–6 minutes) and `postCreateCommand` to finish installing workspace dependencies.
6. Authenticate the three CLIs once — see [First-time CLI authentication](#first-time-cli-authentication) below.
## Windows 11 setup
### Windows-native (one-time setup, then "just works")
The host bind mounts use `${localEnv:HOME}/.claude` (and `.codex`, `.cursor`, `.ssh`, `.config/git`, `.config/gh`, `.gitconfig`). VS Code resolves `${localEnv:HOME}` by reading its own process env, and Windows doesn't set `HOME` by default — it uses `USERPROFILE`. So the bind mounts can't resolve until you tell Windows to also expose your profile as `HOME`.
The `initializeCommand` (`node .devcontainer/ensure-host-config-dirs.cjs`) handles this automatically:
1. **First time you Reopen in Container**, the script detects the missing `HOME`, runs `setx HOME "%USERPROFILE%"` (which writes to your user-level Windows env — no admin needed), prints a one-time setup banner, and exits.
2. **Close all VS Code windows** (File → Exit) and reopen. VS Code picks up the new `HOME` at startup.
3. **Reopen in Container again.** The script now sees `HOME=C:\Users\<you>`, skips the setup block, creates the bind-mount source dirs, and Docker brings the container up.
Subsequent rebuilds work normally with no extra steps. The `HOME` env var is set persistently in your Windows user environment, so it'll be there for every future VS Code session (and any other tool that wants `HOME`).
If you'd rather set it manually before opening the container:
```powershell
setx HOME "%USERPROFILE%"
# Close & reopen VS Code
```
### Known trade-offs of Windows-native vs WSL2
Windows-native works, but Docker Desktop's Windows bind-mount layer has rough edges that WSL2 avoids:
- **File watchers can miss events.** Vite / jest `--watch` running inside the container watching workspace files mounted from `D:\...` may miss changes — chokidar polling (`CHOKIDAR_USEPOLLING=true`) is the usual workaround.
- **`npm install` is 3-5× slower** through the Windows-to-Linux bind-mount translation than on a WSL2-native filesystem.
- **Permission edge cases.** The husky `.husky/_/h` EPERM class we hit earlier in this PR is specific to Windows-side bind mounts changing UID ownership between container runs. `post-create.sh` clears the cache defensively to keep this from being fatal, but it's still a real source of friction.
If you hit any of those and want to migrate to WSL2 later, the steps are below.
### WSL2 (faster, fewer edge cases)
To clone and open the repo inside WSL2:
```bash
# 1. Install WSL2 and a Linux distro if you haven't already.
wsl --install -d Ubuntu
# 2. Enter WSL.
wsl
# 3. Clone the repo inside your WSL2 home directory.
cd ~
git clone https://github.com/abhigyanpatwari/GitNexus.git
cd GitNexus
# 4. Launch VS Code from inside WSL — this opens VS Code attached to the WSL2
# filesystem, so `${localEnv:HOME}` resolves to the WSL user's home and
# subsequent "Reopen in Container" uses the WSL2-side path.
code .
```
Then run **Dev Containers: Reopen in Container**. The workspace will be bind-mounted from `\\wsl$\Ubuntu\home\<user>\GitNexus`, which is fast and gives reliable file-system events. **Make sure Docker Desktop's WSL integration is enabled** for your distro: Docker Desktop → Settings → Resources → WSL Integration → toggle on the distro you cloned into.
## macOS
Open the repo folder in VS Code → **Reopen in Container**. The image is multi-arch; on Apple Silicon you'll pull the `linux/arm64` variant automatically.
## Linux
Same as macOS — open in VS Code and reopen in container. `updateRemoteUserUID: true` (default) shifts the container's `node` user UID/GID to match your host user, so bind-mounted files stay writable without extra setup.
## How CLI state flows from your host
### AI CLIs (Claude Code, Codex, Cursor): copy-once from a read-only host stage + per-container credentials
The three AI CLIs use a **copy-from-read-only-stage topology**: the host's `~/.<cli>` folders (and `~/.claude-mem`) are mounted **read-only** at `/host/.<cli>`, and `post-create.sh` copies out of them into per-container named volumes. Credentials, identity, and single config files are copied on **every** create; the shareable subdirs (plugins, skills, agents, memory, commands, prompts, rules) are copied **once** on first create and then owned by the container. Nothing is bind-mounted read-write into the host's CLI config, so the container can never modify your host setup. Session sub-paths overlay the config volume via their own named volumes (Docker mount precedence — more specific path wins).
| Mount | Source | Target | Mode | Purpose |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Container Claude config dir | _named volume_ `claude-config-${devcontainerId}` | `/home/node/.claude` | rw | Per-container credentials + identity |
| Container Codex config dir | _named volume_ `codex-config-${devcontainerId}` | `/home/node/.codex` | rw | Per-container credentials |
| Container Cursor config dir | _named volume_ `cursor-config-${devcontainerId}` | `/home/node/.cursor` | rw | Per-container credentials |
| Container gh config dir | _named volume_ `gh-config-${devcontainerId}` | `/home/node/.config/gh` | rw | Per-container `gh` auth (`hosts.yml`/`config.yml`) seeded from host stage; in-container login persists |
| **Claude sessions** (overlay on the config volume) | _named volume_ `…-claude-sessions-${devcontainerId}` | `/home/node/.claude/projects` | rw | `--resume` transcripts; survives the `<cli>-config` volume wipe — see [Session resume](#session-resume-across-container-recreation) |
| **Codex sessions** | _named volume_ `…-codex-sessions-${devcontainerId}` | `/home/node/.codex/sessions` | rw | `codex resume` rollouts; SQLite index backfills on recreation |
| **Cursor sessions** | _named volumes_ `…-cursor-sessions-${devcontainerId}`, `…-cursor-projects-${devcontainerId}` | `/home/node/.cursor/chats`, `/home/node/.cursor/projects` | rw | `cursor-agent resume` store (best-effort — layout reverse-engineered) |
| **claude-mem store** | _named volume_ `claude-mem-${devcontainerId}` | `/home/node/.claude-mem` | rw | claude-mem's SQLite DB + Chroma vector store; **seeded once** from `/host/.claude-mem`, then container-private — see note below |
| Host Claude state, read-only stage | `$HOME/.claude` | `/host/.claude` | **read-only** | `post-create.sh` reads credentials + identity from here on container-create |
| claude-mem store, read-only stage | `$HOME/.claude-mem` | `/host/.claude-mem` | **read-only** | `post-create.sh` seeds the claude-mem volume from here on first create |
| Host Codex state, read-only stage | `$HOME/.codex` | `/host/.codex` | **read-only** | Same purpose for Codex |
| Host Cursor state, read-only stage | `$HOME/.cursor` | `/host/.cursor` | **read-only** | Same purpose for Cursor |
| **Claude shareable subdirs** | _seeded into the config volume from_ `$HOME/.claude/{plugins/marketplaces,plugins/cache,skills,agents,memory,commands}` | same under `/home/node/.claude/` | n/a (copy) | **Seed-once** copy from the read-only stage; container owns its copy after |
| **Codex shareable subdirs** | _seeded from_ `$HOME/.codex/{plugins,prompts,memories,skills}` | same under `/home/node/.codex/` | n/a (copy) | **Seed-once** copy (whole `plugins/` dir — no path-bearing registry inside it) |
| **Cursor shareable subdirs** | _seeded from_ `$HOME/.cursor/{plugins/marketplaces,plugins/local,rules,commands,agents,skills}` | same under `/home/node/.cursor/` | n/a (copy) | **Seed-once** copy of the Cursor 2.5 plugin/rules/commands surface |
**What gets seeded once from the host (copy, not bind):**
- **Claude**: `plugins/marketplaces`, `plugins/cache`, `skills/`, `agents/`, `memory/`, `commands/`
- **Codex**: `plugins/` (whole dir), `prompts/`, `memories/`, `skills/`
- **Cursor**: `plugins/marketplaces`, `plugins/local`, `rules/`, `commands/`, `agents/`, `skills/`
On the **first** container-create, `post-create.sh` copies each of these out of the read-only `/host/.<cli>` stage into the per-container config volume, then writes a `.devcontainer-shareable-seeded` marker. On every later rebuild the marker is present, so the copy is skipped and the container keeps whatever it has accumulated. A plugin/skill/agent you install **inside** the container persists across rebuilds; one you add on the **host** after first create won't appear in the container until you remove the config volume and rebuild (see [§ Rebuild / reset](#rebuild--reset)). Nothing here is writable back to the host — `/plugin marketplace add` inside the container installs into the container's own volume copy, not your host `~/.<cli>/plugins/`.
**Single config files are copied on container-create, not bind-mounted** — on Docker Desktop Windows a single-file bind is 9p while the named volume is ext4, and atomic config writes (`tmp` → rename onto target) trip EXDEV (this is what caused Codex's `config/batchWrite failed in TUI`). So these are synced from host on rebuild and the container rewrites its own copy until the next rebuild: `settings.json` + `$HOME/.claude.json` (Claude), `config.toml` (Codex), `cli-config.json` + `mcp.json` (Cursor). `hooks.json` (Cursor) is deliberately **not** synced — Cursor hooks execute shell commands, so sharing them would widen the supply-chain attack surface; add it yourself if you want it.
**Plugin registry files with absolute paths are translated, not copied verbatim** — Claude's `known_marketplaces.json` / `installed_plugins.json` / `plugin-catalog-cache.json` and Cursor's `installed_plugins.json` bake in `C:\Users\…` (Windows) or `/Users/…` (macOS) install paths. `post-create.sh` rewrites those to `/home/node/.<cli>/plugins/…` and writes the result into the named volume, so plugins resolve inside Linux instead of failing with `cache-miss`. This translation is **also seed-once per CLI** — it runs only for a CLI being seeded that create (`translate-plugin-registries.cjs claude cursor`), so it stays consistent with the seed-once `cache/` copy and won't overwrite a plugin you installed inside the container on a later rebuild. Codex needs no translation — its enablement registry is `config.toml` (git URLs + logical keys, no filesystem paths), so its whole `plugins/` dir is copied as-is.
**What stays per-container (in the named volume) and is synced from host on container-create:**
- `.credentials.json` (Claude OAuth tokens), `auth.json` (Codex), `cli-config.json` (Cursor) — credentials
- `~/.claude/.claude.json` (Claude's identity-only file: `userID`, `oauthAccount`, migration tracking) — kept per-container so logging in via container doesn't overwrite host's stored identity
`post-create.sh` runs on every container-create, copies host's credentials into the volume if present, then container manages refresh from there. Sync is "always overwrite if host has the file, otherwise leave container alone". So:
- Host has credentials → container starts logged in.
- Host has no credentials → `claude login` / `codex login --device-auth` / `cursor-agent login` inside container; credentials stay in the named volume across rebuilds (volume is keyed by `${devcontainerId}`, stable for the workspace path).
- `claude logout` inside container clears volume credentials only; host is untouched.
**Why CLAUDE_CONFIG_DIR is intentionally NOT set:** Claude's default `~/.claude` matches the named-volume mount target, so the env var added no behavior — but setting it changed which file Claude reads `hasCompletedOnboarding` from. With it set, Claude reads `$CLAUDE_CONFIG_DIR/.claude.json` (the small identity-only file) and re-onboards every container; without it, Claude reads `$HOME/.claude.json` (copied from the read-only `/host/.claude.json` stage on container-create via `seed-claude-config.cjs`, with `hasCompletedOnboarding: true`).
**Host CLI config is protected from write-through.** The shareable dirs are copied out of a **read-only** stage into the container's own volume, so a compromised npm package in the workspace dep tree — running inside the container — **cannot** write a malicious agent, command, skill, or plugin back to `~/.claude/`, `~/.codex/`, or `~/.cursor/` on the host. The earlier design bind-mounted these read-write and accepted that write-through as the cost of live sync; this design closes it. An even earlier alternative (read-only stage + symlinks) made `/plugin marketplace add` inside the container fail with EROFS; copying into a writable volume avoids that, because the container writes to its own copy rather than a read-only mount. What a compromised dependency can still do is **read** the read-only host stages (`/host/.<cli>`, `/host/.claude-mem`) and the read-only credential mounts and exfiltrate them — there is [no egress firewall yet](#whats-not-included-yet). The cost of the copy model is **divergence**: host edits made after first create don't reach the container until you wipe the config volume and rebuild.
**Refresh-token divergence between rebuilds.** Container's credentials match host's at container-create time; after that, container manages its own refresh until the next rebuild. Anthropic rotates refresh tokens on every use, so an unattended container that hasn't talked to the API in weeks can hit a silent 401 if the host has refreshed since. Re-run `claude login` inside the container, or rebuild, to recover.
**claude-mem is seeded once, then container-private.** The [claude-mem](https://github.com/thedotmack/claude-mem) store (`$HOME/.claude-mem` — a multi-GB SQLite DB `claude-mem.db` + `-wal`/`-shm`, plus a Chroma vector store `chroma/chroma.sqlite3` and its HNSW index binaries) is the one shareable-looking folder that is **deliberately not a host bind**, for the same SQLite reason as sessions below: a multi-GB WAL database over the 9p/virtiofs bind risks unreliable `fcntl` locking and corruption — sharply so if claude-mem ran on the host and in the container against the same DB at once. So it gets its own per-container named volume (`claude-mem-${devcontainerId}`), and `post-create.sh` **seeds it once** from the read-only `/host/.claude-mem` stage _only when the volume has no DB yet_. The first container-create copies the host's store in (a one-time copy, possibly several GB); every later rebuild keeps whatever the container accumulated and skips the copy. The container's memory and the host's **diverge from that seed point** — writes do not flow back — which is the price of keeping SQLite off a shared bind. To re-seed from the host's current store, remove the volume (`docker volume rm claude-mem-<id>`) and rebuild. `ensure-host-config-dirs.cjs` creates an empty `~/.claude-mem` on hosts that never installed claude-mem, so the read-only stage bind always resolves; the seed then finds no DB and the container simply starts with empty memory.
### Session resume across container recreation
`claude --resume`, `codex resume`, and `cursor-agent resume` all read **local** transcript files. Those live _inside_ each CLI's config dir, which is a per-container named volume — so they already survive an ordinary **Rebuild Container**. What they did _not_ survive were the very things this README tells you to do: `docker volume rm <cli>-config-${devcontainerId}` to force a re-login or clear an `EACCES`, a `${devcontainerId}` change, or a full delete-and-recreate. Each of those drops the config volume and takes your session history with it.
So the resume/transcript directories get their **own** named volumes (mount group 6 in `devcontainer.json`), keyed like the `node_modules` volumes (`${localWorkspaceFolderBasename}-…-${devcontainerId}`) and mounted _over_ the config volume at the session sub-paths:
| Resume command | Persisted volume → target | What's stored |
| -------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `claude --resume` / `--continue` | `…-claude-sessions-…` → `~/.claude/projects` | `<encoded-cwd>/<uuid>.jsonl` transcripts + `sessions-index.json`. Container cwd is always `/workspace`, so only that slice is stored. Pure JSONL/JSON — no SQLite. |
| `codex resume` / `resume --last` | `…-codex-sessions-…` → `~/.codex/sessions` | `YYYY/MM/DD/rollout-*.jsonl`. The `state_5.sqlite` thread index stays on the config volume (a single WAL file we don't split out); when it's absent after a recreation, Codex rebuilds it from these rollouts on the next start (a one-time backfill). |
| `cursor-agent resume` / `ls` | `…-cursor-sessions-…` → `~/.cursor/chats`; `…-cursor-projects-…` → `~/.cursor/projects` | `chats/{hash}/{uuid}/store.db` (one SQLite db per session, each in its own dir) + `projects/.../agent-transcripts`. cursor-agent's layout is reverse-engineered, so treat this as best-effort. |
Because these are **separate** volumes from `<cli>-config-${devcontainerId}`, the re-login fix (`docker volume rm claude-config-…`) no longer destroys your sessions — that was the point.
**Survives:** Rebuild Container, Rebuild Without Cache, a full delete-and-recreate of the container, and the `docker volume rm <cli>-config-…` re-login / `EACCES` fix.
**Does _not_ survive** (same durability tier as the `node_modules` volumes): `docker volume prune`, a `${devcontainerId}` change (moving the checkout to a new path, or switching between Windows-native and WSL2), or moving to a new machine. To deliberately wipe sessions, remove the session volumes too — see [Rebuild / reset](#rebuild--reset). Two checkouts with the **same folder name** on one host would share session volumes only if they also share a `${devcontainerId}`; they don't, so they stay separate.
**First rebuild after adopting this, one-time:** if a container created _before_ these volumes existed already had sessions on the config volume (`~/.claude/projects`, `~/.codex/sessions`, …), the new empty session volume mounts _over_ that sub-path and **masks** the old content — same Docker-precedence shadowing described for plugins above. The old sessions are hidden, not deleted. To carry them forward once, copy them out of the config volume into the session volume; or just start fresh — new sessions land on the session volume from then on.
**Why sessions are container-private and not even seeded from the host.** The shareable config dirs are _seeded once_ from the host (you want your skills/agents/plugins in the container). Sessions are deliberately _not_ seeded and never touch the host, because a transcript can contain anything you pasted or the agent read — API keys, file contents, connection strings. Binding or copying them to/from the host would (a) spill that to host disk, (b) add a write-through surface a compromised dependency can reach (there's still [no egress firewall](#whats-not-included-yet)), and (c) leak _every other project's_ transcripts into the container (Codex `sessions/` and Cursor `chats/` aren't project-scoped). Container-private volumes avoid all three while still surviving recreation. And Claude/Codex transcripts embed the container cwd (`/workspace`), so even if you _did_ bind them to the host, the host CLI wouldn't natively `--resume` them — its encoded-cwd folder differs.
**Opt in to host-shared sessions anyway.** If you want transcripts visible/portable on the host and accept the trade-offs above, uncomment the host-bind block in `devcontainer.json` (just below the group-6 volumes) and add the matching source dirs to `ensure-host-config-dirs.cjs`'s `DIRS` so Docker can resolve the binds. That block scopes Claude to `/workspace`'s encoded subdir to limit the cross-project leak; the Codex and Cursor stores can't be scoped that way, so they expose every project's transcripts.
### Other host bind mounts
| Container path | Host source | Mode | Why |
| --------------- | ------------------- | ------------- | -------------------------------------------------------------------------------------- |
| `~/.config/git` | `$HOME/.config/git` | **read-only** | XDG-style git config / ignore / attributes |
| `~/.ssh` | `$HOME/.ssh` | **read-only** | SSH commit signing + git push over SSH |
| `~/.config/gh` | `$HOME/.config/gh` | **copy → volume** | `gh` CLI auth (PR/issue create, checks) — seeded from your host login on create into a per-container volume; in-container `gh auth login` persists across rebuilds and never writes back to the host |
| `~/.docker` | `$HOME/.docker` | **read-only** | Container registry auth + buildx config (inert until you add Docker CLI via a Feature) |
| `~/.aws` | `$HOME/.aws` | **read-only** | AWS CLI / SDK credentials (forward-compat — empty by default) |
| `~/.azure` | `$HOME/.azure` | **read-only** | Azure CLI credentials (forward-compat — empty by default) |
**Why `ssh`/`aws`/`azure`/`docker` are read-only, and why `gh` is copied into a volume:** `ssh`/`aws`/`azure` are consumed read-only by their clients (the SSH client and the AWS/Azure SDKs only read their credential files), so a one-way mount loses nothing. `docker` _can_ write its own state (`docker login` / buildx write `config.json`), but a read-write host bind would let a compromised in-container dependency rewrite your host `~/.docker/config.json` (point a `credHelper` at an attacker-controlled binary) — a credential-takeover vector. The common case is _reading_ an existing host login, so `docker` stays **read-only**: registry pulls/pushes using your host creds work, only a `docker login` inside the container won't persist back. `gh` used to be read-only for the same reason, but that meant an in-container `gh auth login` had nowhere to write and silently failed. So `gh` now uses the **copy-into-volume** model (the same one the AI-CLI credentials use): the host `~/.config/gh` is a read-only _stage_ at `/host/.config/gh`, and `post-create.sh` copies `hosts.yml`/`config.yml` out of it into the per-container `gh-config` volume on create. The container gets a **writable** copy — `gh auth login` / `gh auth refresh` inside the container now work and persist across rebuilds — while the read-only stage guarantees nothing is ever written back to the host's token. If you want `docker` to behave the same way, give it the same treatment (a `/host/.docker` stage + a docker-config volume + a copy step in `post-create.sh`).
`~/.gitconfig` is **not** bind-mounted — VS Code's Dev Containers extension auto-copies the host's gitconfig into the container at attach time (this is built-in behavior, not something this devcontainer configures). The bind-mount approach conflicts with that auto-copy mechanism, so we let VS Code own it. The end result is the same: your host's `user.name` / `user.email` are available inside the container.
If a host source dir doesn't exist when the container is first created, the `initializeCommand` (`node .devcontainer/ensure-host-config-dirs.cjs`) creates it empty — so the bind mount always has a valid source.
### Per-CLI quirks worth knowing
- **Claude Code on macOS** stores credentials in the system Keychain, not in `~/.claude/.credentials.json`. The sync silently no-ops; run `claude login` inside the container once and the named volume persists it.
- **Codex on macOS / Linux with `cli_auth_credentials_store = "keyring"`** stores auth in the OS keyring (Keychain / Secret Service), so `~/.codex/auth.json` may not exist on host. Same fallback: `codex login --device-auth` inside the container.
- **Cursor CLI inside containers** has [known upstream auth issues](https://forum.cursor.com/t/cursor-agent-authentication-issue-inside-docker/143995) — even with a correctly-synced `cli-config.json`, you may need to re-run `cursor-agent login` inside the container.
- **Stale named volumes from old rebuilds can carry forward.** If you delete and re-create the same workspace, or if a prior container left interim state with a different `userID`, deleting the named volumes before rebuild guarantees a clean sync: `docker volume rm claude-config-${devcontainerId} codex-config-${devcontainerId} cursor-config-${devcontainerId}` (look them up with `docker volume ls | grep -config-`).
- **User-scope MCP servers with absolute host paths won't resolve in-container.** `~/.claude.json` (Claude), `~/.codex/config.toml` (Codex), and `~/.cursor/mcp.json` (Cursor) are copied from host on container-create, so their user-scope `mcpServers` entries come along. But an entry whose `command` is an absolute host path (`C:\tools\foo.exe`, `/usr/local/bin/foo`) points at a binary that doesn't exist in the container — that server silently fails to launch. Only registry/`npx`-based servers (like this repo's `.mcp.json`, which uses `npx -y gitnexus@latest mcp`) and remote/URL servers work unchanged. The path-translation pass only rewrites `*/.​<cli>/plugins/*` registry paths, **not** arbitrary `mcpServers` command paths (there's no correct container target for a host-local binary). Install such MCP servers inside the container, or use `npx`/remote ones.
- **Host config is seeded once per devcontainer, then diverges — this now applies to everything.** A `mcpServers` entry, setting, plugin, skill, agent, or command you add **on the host after** the container was created is not visible in the container until you remove the config volume and rebuild. Single config files (`mcpServers`, `settings.json`, …) are copy-on-create; the shareable dirs (plugins/skills/agents/memory/commands/prompts/rules) are copy-on-**first**-create (they persist across ordinary rebuilds and aren't even re-copied). Both diverge from the host after their copy. To pull host-side changes in, wipe the relevant volume and rebuild (see [§ Rebuild / reset](#rebuild--reset)).
- **Plugins/skills/agents installed in-container persist; they do not reach the host.** A `/plugin marketplace add` (or `codex plugin add`, or a new skill/agent) inside the container writes to the container's own config volume and survives ordinary rebuilds. It never appears on the host — the host dirs are read-only sources, not bind targets. To get a plugin onto the host, install it on the host (then wipe + rebuild to seed it into the container).
- **No cross-checkout plugin contention.** Because each container copies plugins into its own per-`${devcontainerId}` volume rather than sharing one host bind source, two containers (or checkouts) installing plugins at the same time no longer interleave git clones/extractions against a shared host dir. Each writes only its own copy.
### What you still don't have inside the container
These are commonly-needed CLIs that aren't installed by default — adding them would be follow-up work, not in this PR's scope:
- **Docker CLI** (for `docker push` / `docker build` from inside the container). Add via `ghcr.io/devcontainers/features/docker-outside-of-docker:1` to the `features` block — `~/.docker/` is already mounted **read-only**, so your host `docker login` state works immediately for pulls/pushes; an in-container `docker login` won't persist to the host (drop `,readonly` on that mount if you need it to).
- **AWS CLI / Azure CLI / gcloud / kubectl** — same pattern: add the matching Feature, the host config dirs already flow through.
- **Private npm registry auth** (`~/.npmrc`) — you don't have a global one on this host. If you ever start using private packages, add `source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,readonly` to the mounts.
That means:
- **Authentication is shared.** If you're already logged in on the host (`claude login`, `codex login`, `cursor-agent login`, `gh auth login`), you're already logged in inside the container. No second login step.
- **Plugins, skills, agents, memory, and commands are seeded from the host once, then container-private.** On first create the container copies your host's plugins/skills/agents/memory/commands (and Codex prompts/memories, Cursor rules) into its own volume. After that they're independent: install or edit inside the container and it stays in the container (persists across rebuilds); add a plugin or agent on the host and the container won't see it until you wipe the config volume and rebuild. Nothing the container does reaches the host. (`settings.json` and the user-scope `~/.claude.json` are copy-on-create the same way; `~/.claude/projects/` is container-local by design.)
- **Git identity comes from the host.** Commits from inside the container use your host's `user.name` / `user.email` — VS Code's Dev Containers extension auto-copies your `~/.gitconfig` into the container at attach time. Any XDG-style config under `~/.config/git/` flows through via the read-only bind mount. To change git identity, edit `~/.gitconfig` on the host (container-side `git config --global` writes to a container-local file that's discarded on rebuild).
- **SSH keys flow through (read-only).** Push over SSH remotes and SSH commit signing work inside the container using your host keys. The mount is read-only so container code can't exfiltrate or modify private keys — agent-perspective, this means you get git operations but the keys stay vendor-side.
- **`gh` auth is shared, and in-container logins persist.** If you're logged in on the host, `gh pr create`, `gh pr checks`, `gh issue create` work inside the container without re-authenticating. If you're not, run `gh auth login` inside the container once — because `gh` config lives in a writable per-container volume (seeded from the host stage), that login persists across rebuilds and never touches the host's token.
- **No per-workspace duplication.** All your devcontainers across all your projects see the same host CLI state, just like all your host shells do.
The bind mount source directories are guaranteed to exist by the `initializeCommand` (`node .devcontainer/ensure-host-config-dirs.cjs`), which runs on the host before container create. It's a Node script (not a shell one-liner) so the same command works on Windows `cmd.exe` and POSIX shells. It creates the top-level bind-mount source dirs — `~/.claude`, `~/.codex`, `~/.cursor`, `~/.claude-mem`, plus `~/.ssh`, `~/.docker`, `~/.aws`, `~/.azure`, `~/.config/{gh,git}`. It deliberately does **not** pre-create the shareable subdirs (skills/agents/plugins/…): those are no longer bind sources (they're copied out of the whole-`~/.<cli>` read-only stage), and pre-creating empty ones would needlessly write into the host of someone who never used that CLI.
### Trust boundary, concretely
Host and container share a single trust boundary by design — fine for personal-dev, but the consequence is concrete. Any malicious npm package or `postinstall` script in the workspace dep tree, running inside the container, has direct **read** access to:
- **Host AI CLI state** — the read-only stage at `/host/.claude`, `/host/.codex`, `/host/.cursor`, `/host/.claude-mem`, which exposes your **entire** host `~/.<cli>` tree (credentials, identity, AND the shareable skills/agents/plugins/memory/commands) for _reading_. The container copies what it needs out of this stage; a compromised dep can read all of it. It is read-only, so none of it can be written back
- The **container's own credential snapshots** at `/home/node/.claude/.credentials.json` etc. (copied from host on container-create)
- `~/.claude/memory/` / per-project memory (which may contain user-stored secrets if you've used the `/remember` skill)
- The **current container's own session transcripts** (`~/.claude/projects`, `~/.codex/sessions`, `~/.cursor/chats`/`projects` — the group-6 volumes), which can hold anything pasted into or read during a session. These are container-private (see one-way note below), so this is read access to _this_ container's sessions only, not the host's or other projects'
- Your **`gh` token** (`~/.config/gh`)
- Your **SSH private keys** (`~/.ssh/`)
- Docker registry tokens in **`~/.docker/config.json`** (if you've `docker login`-ed)
- AWS/Azure CLI credentials if you've populated `~/.aws/` or `~/.azure/`
It does **not** have write-through to the host's CLI config. The shareable dirs are copied out of the read-only stage into the container's own volume, so a compromised in-container dep **cannot** write into your host `~/.claude/{plugins,agents,skills,commands,memory}/`, `~/.codex/{plugins,prompts,memories,skills}/`, or `~/.cursor/{plugins,rules,commands,agents,skills}/`. The persistence vector earlier versions had — drop a malicious auto-loaded agent/command/skill/rule onto the host, have it run in your next **host** session — is closed: there is no writable path from the container to those host folders. (Cursor's `hooks.json` is still additionally withheld from even the _container's_ copy, because hooks fire without an agent invoking them.) The boundary is now one-way for **all** of the host CLI config, not just credentials.
**What stays one-way (genuinely protected):** everything. Credentials never flow back to host — `.credentials.json` / `auth.json` / `cli-config.json` live only in the per-container named volumes, and the `/host/.<cli>` stage they're copied from is mounted **read-only**, so the snapshot can't be overwritten back. The shareable AI-CLI dirs (skills/agents/plugins/memory/commands/prompts/rules) are now copy-on-create from that same read-only stage, so they have the one-way property too — readable for the copy, never writable back. `~/.ssh`, `~/.config/git`, `~/.aws`, `~/.azure`, and **`~/.docker`** are read-only binds with the same property — a compromised dep can _read_ your registry tokens but cannot _rewrite_ them to hijack your future host auth. **`~/.config/gh`** is now a read-only _stage_ copied into a per-container volume, so it keeps that same one-way property: the container reads it once to seed its own writable copy, and the read-only stage means an in-container `gh auth login` can never overwrite your host token. **Session transcripts** live in per-workspace named volumes (mount group 6) and are never seeded from or written back to the host, and the container can't see any _other_ project's transcripts. The opt-in host-bind block in `devcontainer.json` reverses that for sessions only — enable it only if you accept transcripts on host disk; see [Session resume across container recreation](#session-resume-across-container-recreation).
**The egress firewall is the key compensating control that is still missing.** It's deferred (see "What's not included (yet)" below), so a compromised package currently has unrestricted outbound network to exfiltrate anything in the read list above. Until it lands, treat that read surface as exposed to any code you run in the container — don't use this devcontainer on a machine whose host credentials you couldn't afford to rotate. The isolated-volume setup below removes host AI-CLI config/credentials from that surface entirely.
**If a workspace dep is ever found compromised**, rotate credentials at the vendor side — local file deletion is insufficient because tokens may have already left:
- Anthropic: [console.anthropic.com → Settings → Keys](https://console.anthropic.com/settings/keys), revoke the OAuth session under Account
- OpenAI / Codex: [platform.openai.com/api-keys](https://platform.openai.com/api-keys), revoke session under Profile
- Cursor: dashboard → Integrations, rotate API key + revoke CLI session
- GitHub: `gh auth refresh` or revoke the token at github.com/settings/tokens
For high-trust enterprise environments where the container should not even be able to **read** host CLI state, remove the three read-only stage binds (`/host/.claude`, `/host/.codex`, `/host/.cursor`) — plus `/host/.claude-mem` and `/host/.claude.json` — from `.devcontainer/devcontainer.json`. With no stage to copy from, `post-create.sh`'s seed and credential-sync steps quietly do nothing (their `[ -f ]` / `[ -d ]` guards), and each devcontainer starts with empty, fully isolated config and credentials (Anthropic's reference pattern). You give up seeding your host setup into the container in exchange for removing host config/credentials from the container's read surface entirely; log in inside each container instead.
## First-time CLI authentication
Each CLI works either way:
- **Log in on host first** → the container picks it up automatically on the next rebuild (`sync_from_host` copies the credential file into the named volume during `post-create.sh`). Host stays the source of truth.
- **Log in inside the container** → credentials write to the named volume. They persist across ordinary rebuilds (volume is keyed by `${devcontainerId}`, which is stable for a given workspace folder). The host's credentials are untouched.
You can mix and match per-CLI. A common setup is "Claude logged in on host, Codex/Cursor logged in inside container".
### Claude Code
```bash
claude login
```
Opens a browser auth flow. VS Code's port forwarding handles the OAuth callback automatically. After auth, `~/.claude/` is populated and visible from both host and container. The `DISABLE_AUTOUPDATER=1` env var prevents the in-container CLI from auto-updating — rebuild the container to pick up a newer Claude Code.
### OpenAI Codex CLI
```bash
codex login --device-auth
```
The device-code flow prints a URL and a one-time code. Visit the URL on your host browser, paste the code, and the CLI authenticates without needing a callback listener — this is the most reliable path inside containers. Credentials land in `~/.codex/auth.json` (shared with host).
`codex login` (browser-callback variant) also works but can be flaky in some headless contexts; prefer `--device-auth`.
### Cursor CLI
```bash
cursor-agent login
```
Opens a browser auth flow; VS Code's port forwarding handles the callback. Credentials persist in `~/.cursor/cli-config.json` (shared with host).
Verify any time with `cursor-agent status`.
## Alternative: API key authentication (CI / headless)
For non-interactive use (CI runners, automated scripts), all three CLIs accept API keys via env vars:
| CLI | Env var | Where to get the key |
| ----------- | ------------------- | --------------------------------------------- |
| Claude Code | `ANTHROPIC_API_KEY` | <https://console.anthropic.com/settings/keys> |
| Codex | `OPENAI_API_KEY` | <https://platform.openai.com/api-keys> |
| Cursor | `CURSOR_API_KEY` | Cursor dashboard → Integrations |
These env vars are intentionally **not** injected into the container from the host. `${localEnv:VAR}` resolves an unset host variable to an empty string, and some CLIs (Cursor in particular) treat a set-but-empty key as "use this key" rather than "fall back to stored login" — which would silently break the login flow for everyone who hasn't pre-set the host var.
To use an API key inside the container, export it in your terminal session:
```bash
export ANTHROPIC_API_KEY=sk-ant-...
# or OPENAI_API_KEY, or CURSOR_API_KEY
```
For persistence across container shells, carry the export via your VS Code [dotfiles repository](https://code.visualstudio.com/docs/devcontainers/containers#_personalizing-with-dotfile-repositories). VS Code clones the dotfiles repo into the container on attach and runs your install command, so the export lands in `~/.bashrc` / `~/.zshrc` per your own setup — and your API keys stay out of this repo's committed `devcontainer.json`.
A non-empty API key env var takes precedence over stored login credentials for each CLI.
## Port forwarding
| Port | Service | Notes |
| ------ | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `5173` | Vite dev server (`gitnexus-web`) | Auto-forwarded with notification |
| `4747` | `gitnexus serve` HTTP API | **Must not be remapped** — `gitnexus-web` hardcodes `http://localhost:4747` as the default backend URL |
| `4173` | Static web (Vite preview) | Silently forwarded |
VS Code's Ports panel shows forwarded ports once their listener starts.
## Known gotchas
- **LadybugDB integration tests may fail in containers** (file-locking, `AGENTS.md` § Testing). Default to `npm run test:unit` inside the container; run integration tests on the host. Tracking issue: documented as a known limitation.
- **Single-writer LadybugDB constraint** (`GUARDRAILS.md` § LadybugDB lock). Don't run `gitnexus analyze` on the host and inside the container against the same `.gitnexus/` directory simultaneously — the second writer will get `database busy`.
- **Native grammar builds add ~30s to first install.** Tree-sitter Dart/Proto/Swift grammars build during `gitnexus`'s `postinstall`. To skip them (loses parsing for those three languages), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` in your shell or add it to `remoteEnv` and rebuild.
- **`tree-sitter-kotlin` warnings on install** are expected (per `AGENTS.md`). Ignore them.
- **`.mcp.json` works inside the container**: `npx -y gitnexus@latest mcp` resolves cleanly because npm registry is reachable and the workspace bind mount exposes the same `.mcp.json` the host sees.
- **Husky pre-commit fires inside the container** without extra setup. The root `npm install` (run automatically in `postCreateCommand`) installs the hook via `package.json` `prepare`.
## Rebuild / reset
- **Rebuild Container** (Command Palette) — re-runs the Dockerfile build and `postCreateCommand` against the existing named volumes (auth, history, **and sessions** persist).
- **Rebuild Container Without Cache** — fresh image layers, same volumes.
- **To force a re-login / clear an `EACCES`** — remove the per-container _config_ volumes and rebuild. As of the session-volume change this **no longer drops your `--resume` history** (sessions are on separate volumes — see [Session resume](#session-resume-across-container-recreation)):
```bash
docker volume ls | grep -- -config- # the credential / identity volumes
docker volume rm claude-config-<id> codex-config-<id> cursor-config-<id> gh-config-<id>
```
⚠️ Since the shareable dirs are now seeded into the config volume (not bind-mounted), wiping `<cli>-config` **also discards any plugin/skill/agent/command you installed _inside_ the container** and re-seeds those dirs from the host on the next rebuild. That is the intended way to pull host-side config changes in, but if you have in-container-only plugins you want to keep, reinstall them after the rebuild (or install them on the host first so the re-seed brings them along).
- **To also wipe session history** (a true clean slate) — remove the session volumes too (`<name>` is your workspace folder name):
```bash
docker volume ls | grep -E -- '-(sessions|cursor-projects)-' # the group-6 volumes
docker volume rm <name>-claude-sessions-<id> <name>-codex-sessions-<id> \
<name>-cursor-sessions-<id> <name>-cursor-projects-<id>
```
Then rebuild.
- **To re-seed claude-mem from the host** (the container's memory has diverged and you want the host's current store back) — remove the claude-mem volume and rebuild; `post-create.sh` copies the host store in again on the next create:
```bash
docker volume rm claude-mem-<id>
```
## Bumping CLI versions
Bump the version pins in `.devcontainer/devcontainer.json` `build.args` and rebuild — all three are real, fail-loud pins. Claude Code installs via `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}` and Codex via `npm install -g @openai/codex@${CODEX_VERSION}`. **Cursor is pinned too:** bump `CURSOR_VERSION` **and** both `CURSOR_SHA256_X64` / `CURSOR_SHA256_ARM64` together — the Dockerfile downloads the pinned `downloads.cursor.com/lab/<version>/linux/<arch>/agent-cli-package.tar.gz` artifact directly (no remote install script) and fails the build on a sha256 mismatch. Re-hash each arch with `curl -fSL <url> | sha256sum`. To stop Cursor from auto-updating in the running container, don't call `cursor-agent update`.
## What's not included (yet)
- **Egress firewall — the most important hardening still outstanding.** The original plan included an opt-in iptables/ipset firewall adapted from Anthropic's reference devcontainer. It was deferred to a follow-up PR — `runArgs` is static in `devcontainer.json`, so toggling NET_ADMIN/NET_RAW capabilities cleanly requires either a separate `devcontainer-firewall.json` profile or an `initializeCommand`-generated overlay. Until it lands, the read surface in [§ Trust boundary](#trust-boundary-concretely) has no network containment — anything readable can be exfiltrated. Track at the project's issue tracker if you need this.
- **Codespaces tuning.** The current config works in Codespaces incidentally (no privileged capabilities, no host-mount assumptions), but isn't actively tested there.
- **Playwright e2e support.** `gitnexus-web`'s `npm run test:e2e` needs Chromium libs that the base image doesn't ship. Use the host for e2e until a Playwright layer is added.
## Troubleshooting
| Symptom | Likely cause | Fix |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GitNexus devcontainer one-time Windows setup` banner from `initializeCommand` | First-time Windows-native Reopen-in-Container; `HOME` env var was missing | The script just ran `setx HOME "%USERPROFILE%"` for you. Close ALL VS Code windows (File → Exit) and reopen — see [Windows 11 setup](#windows-11-setup) |
| `bind source path does not exist: /.claude` (or similar) from Docker | Windows-native `HOME` env var is still missing even after one rebuild — `setx` may have failed or VS Code wasn't fully restarted | Run `setx HOME "%USERPROFILE%"` in a Windows shell manually, fully exit VS Code (check Task Manager that no `Code.exe` remains), reopen |
| `EACCES` / `EPERM` writing into `~/.claude`, `~/.codex`, or `~/.cursor` inside the container | Stale state from a previous container with a different effective UID | Move the affected dir aside and let the CLI rebuild it (`mv ~/.claude ~/.claude.bak` and log in again). Long-term: WSL2 setup, which doesn't hit this class of issue |
| `EPERM: operation not permitted, copyfile ... '.husky/_/h'` in `postCreateCommand` | Leftover `.husky/_/` from a previous container run on a Windows-side bind mount | `post-create.sh` already runs `rm -rf .husky/_` defensively. If you hit this on an older config, delete `.husky/_/` on the host and rebuild. Long-term: clone in WSL2 |
| Vite never hot-reloads | Repo cloned on Windows side, not WSL2 | Re-clone inside WSL2 |
| `gitnexus-web` can't reach the backend | `4747` was remapped or backend isn't running | Verify the Ports panel shows `4747` forwarded with no remap; start the backend with `cd gitnexus && npx gitnexus serve` |
| `npm install` fails on tree-sitter-swift / proto / dart | Native build toolchain missing | This shouldn't happen in the devcontainer — verify the apt layer installed `python3 make g++`. If iterating, set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` to skip the vendored grammars |
| Integration tests fail with `database busy` | LadybugDB single-writer constraint | Don't run host-side `gitnexus analyze` while the container is also analyzing the same repo; choose one writer |
| API key env vars not visible inside the container | They are intentionally not auto-propagated from the host (so an empty/stale host var can't silently break `*-login` for everyone else) | `export ANTHROPIC_API_KEY=...` / `OPENAI_API_KEY=...` / `CURSOR_API_KEY=...` inside the container shell, or carry it via your VS Code [dotfiles repo](https://code.visualstudio.com/docs/devcontainers/containers#_personalizing-with-dotfile-repositories) for persistence |
| `git commit` produces commits with empty author | `~/.gitconfig` is missing or empty on the host (VS Code's auto-copy had nothing to copy) | Set `git config --global user.name "Your Name"` and `git config --global user.email "you@example.com"` from the host shell, then rebuild the container |
| `gh: not logged in` inside the container | Not logged in on the host (nothing to seed), or the `gh-config` volume is empty | Just run `gh auth login` **inside the container** — `gh` config lives in a writable per-container volume, so the login persists across rebuilds. (Logging in on the host instead also works: it seeds in on the next container create.) |
+9
View File
@@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
}
}
}
+396
View File
@@ -0,0 +1,396 @@
// Devcontainer for GitNexus. It pre-installs Claude Code, the OpenAI Codex
// CLI, and the Cursor CLI, plus the Node.js native build chain. It works on
// macOS, Linux, Windows via WSL2, and Windows native. Windows native needs a
// one-time HOME setup. That setup runs automatically via initializeCommand.
// See .devcontainer/README.md § Windows 11 setup. Open it with the VS Code
// Dev Containers extension.
//
// For first-time setup, auth flows, and troubleshooting, see
// .devcontainer/README.md.
{
"name": "GitNexus AI CLI Devcontainer",
"build": {
"dockerfile": "Dockerfile",
"context": ".",
"args": {
"CLAUDE_CODE_VERSION": "2.1.156",
"CODEX_VERSION": "0.134.0",
// Cursor: a pinned version plus one sha256 hash per CPU arch. The
// Dockerfile checks the tarball against the hash at build time, so it
// never runs a remote install script. Bump all three values together.
// Re-hash each arch with:
// curl -fSL https://downloads.cursor.com/lab/<ver>/linux/<x64|arm64>/agent-cli-package.tar.gz | sha256sum
"CURSOR_VERSION": "2026.05.28-a70ca7c",
"CURSOR_SHA256_X64": "7f8b6a09393e0b84b288cc6952b292fc98d15775f644cc01b0b9aa4f04b268df",
"CURSOR_SHA256_ARM64": "05a0ab361e038729aba25fe7f407531b3e8432912e499d0bffdf1dda0e7833e9",
// Bun: pinned by version. Installed by the official bun.sh/install
// script, which accepts the release tag as its first positional arg
// (`bash -s bun-vX.Y.Z`). UNLIKE Cursor, the install path runs an
// unverified remote script — chosen at request time for simplicity.
// To bump: pick a tag from github.com/oven-sh/bun/releases and update
// this value.
"BUN_VERSION": "1.3.14",
"TZ": "${localEnv:TZ:UTC}"
}
},
// Runs on the HOST, not the container, before the container is created. We
// write it as a single string on purpose. The spec treats the single-string
// form as one command that each OS runs its own way. The object form means
// "named parallel tasks", not per-OS dispatch. We run it with Node so the
// same command works in cmd.exe on Windows and in bash/zsh on Linux, macOS,
// and WSL. The script reads `os.homedir()`, which respects $HOME on
// Linux/macOS and %USERPROFILE% on Windows. It then creates the host-side
// bind mount source folders, and it is safe to re-run. Host prerequisite:
// Node on PATH. That is the only host-side tool needed beyond Docker Desktop
// and the VS Code Dev Containers extension.
"initializeCommand": "node .devcontainer/ensure-host-config-dirs.cjs",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"remoteUser": "node",
"updateRemoteUserUID": true,
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
"workspaceFolder": "/workspace",
// Mount topology, by group:
//
// 1. AI CLI host config — a READ-ONLY stage at /host/.<cli>. On container-
// create, `post-create.sh` COPIES out of it: credentials, identity, and
// single config files (always), plus the shareable subfolders (Claude
// plugins/skills/agents/memory/commands; Codex plugins/prompts/memories/
// skills; Cursor plugins/rules/commands/agents/skills) ONCE on first create.
// Everything copied lands in the per-container named volume (role 2). It is
// read-only so a container process can NEVER write back to the host — there
// is no read-write bind into the host's CLI config at all. This protects the
// host's on-disk setup: a compromised in-container dependency cannot drop a
// skill, agent, command, or plugin onto the host for the next host session
// to load. The cost is that host and container DIVERGE after the first
// create — host edits don't reach the container until you wipe the config
// volume and rebuild. See README § "Trust boundary, concretely".
//
// 2. AI CLI container config — one named volume per devcontainer. CODEX_HOME
// points here. CLAUDE_CONFIG_DIR is left unset on purpose, so it resolves
// to the default ~/.claude, which is this same path. Credentials,
// identity, and single config files (.credentials.json,
// ~/.claude/.claude.json, settings.json, config.toml, cli-config.json,
// mcp.json) live here with correct Linux permissions. They are NOT
// bind-mounted, because single-file binds break on Docker Desktop Windows
// (the EXDEV error — see the SINGLE-FILE note below). Container-managed
// state (sessions, history, caches, IDE locks) stays separate per
// devcontainer. So two GitNexus checkouts on the same host can't corrupt
// each other.
//
// 3. Other host config — read-only bind mounts for credential and identity
// folders that lack the permission-flattening and onboarding-state
// complications Claude Code has (ssh, aws, azure, git config, plus gh and
// docker). gh and docker are read-only so a compromised dependency can't
// rewrite the GitHub token or the Docker credHelper. See the inline note
// at those mounts. `~/.gitconfig` is not mounted here. VS Code auto-copies
// it separately.
//
// 4. Per-instance state — scoped by `${devcontainerId}`: shell history and
// the npm cache. These survive rebuilds and stay separate between sibling
// instances.
//
// 5. Per-workspace-name AND per-instance state — the workspace `node_modules`
// volumes use both `${localWorkspaceFolderBasename}` (so you can spot them
// in `docker volume ls`) and `${devcontainerId}` (so sibling instances of
// the same repo never collide). This keeps tree-sitter native binaries and
// onnxruntime off the workspace bind mount, which is faster on Windows and
// macOS.
//
// 6. Per-workspace session state — dedicated named volumes for each CLI's
// resume/transcript dirs (Claude projects/, Codex sessions/, Cursor chats/
// + projects/). Same `${localWorkspaceFolderBasename}` + `${devcontainerId}`
// keying as group 5, but SEPARATE volumes from the group-2 config volumes.
// That separation is the point: the `docker volume rm <cli>-config-*`
// re-login / EACCES fix (README § Rebuild/reset) no longer wipes sessions,
// so `claude --resume`, `codex resume`, and `cursor-agent resume` survive a
// rebuild, a full delete-and-recreate, AND that wipe. They overlay the
// config volume at the session sub-paths (Docker precedence: more specific
// path wins). Container-private by design — transcripts can hold pasted
// secrets, and like the group-1 config (read-only stage, copy-once) these
// add NO host write-through surface and leak no other projects' transcripts.
// They do
// NOT survive `docker volume prune`, a `${devcontainerId}` change (moving
// the checkout, WSL vs native), or a new machine — same tier as group 5.
// To make sessions host-visible/portable instead, see the commented
// host-bind block below and README § "Session resume across recreation".
"mounts": [
// One named volume per container for credentials and identity state. Each
// CLI's real `~/.<cli>` config folder lives in a volume. That keeps
// credentials (with correct Linux 600 permissions) and per-container
// session state separate from the host. Logging in inside the container
// and logging in on the host are independent. The bind mounts BELOW these
// volumes override the volume's contents at the paths they cover. Docker
// mount precedence is: the more specific path wins.
"source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
"source=codex-config-${devcontainerId},target=/home/node/.codex,type=volume",
"source=cursor-config-${devcontainerId},target=/home/node/.cursor,type=volume",
// gh CLI config as a per-container named volume, same model as the AI CLI
// configs above: post-create.sh COPIES hosts.yml/config.yml out of the
// read-only /host/.config/gh stage into this volume on container-create.
// The container then owns a WRITABLE copy, so `gh auth login` /
// `gh auth refresh` run INSIDE the container persist across rebuilds — and
// still never write back to the host (the stage is read-only). If the host
// is logged in, that login seeds in; if not, an in-container login sticks.
"source=gh-config-${devcontainerId},target=/home/node/.config/gh,type=volume",
// claude-mem store. UNLIKE the shareable dirs below (skills/agents/memory),
// this is NOT a host bind. $HOME/.claude-mem is a large, multi-GB SQLite +
// Chroma vector store (claude-mem.db + -wal/-shm, chroma/chroma.sqlite3, HNSW
// index binaries). A read-write host bind would (a) push every byte over the
// 9p/virtiofs share, and (b) expose those SQLite WAL files to unreliable
// fcntl locking across that boundary — with a real corruption risk if
// claude-mem ran on the host and in the container against the same DB at
// once. So it gets its OWN per-container named volume here, same durability
// tier as the config volumes (survives Rebuild Container and a
// delete-and-recreate; keyed by ${devcontainerId}). post-create.sh SEEDS it
// ONCE from the /host/.claude-mem read-only stage when the volume is empty,
// then the container owns its copy — rebuilds never clobber it, and changes
// do NOT flow back to the host. (Container and host memory diverge from the
// seed point on; that is the price of safe SQLite.) Removed by the same
// `docker volume rm` reset flow as the other volumes — see README.
"source=claude-mem-${devcontainerId},target=/home/node/.claude-mem,type=volume",
// Per-workspace SESSION volumes (mount group 6). These OVERLAY the config
// volumes above at the session sub-paths so "resume my last session"
// survives container recreation the way the seeded config dirs do.
// They are SEPARATE volumes from <cli>-config-${devcontainerId}, so the
// README's `docker volume rm <cli>-config-${devcontainerId}` re-login fix
// does not touch them. post-create.sh chowns each one explicitly (its
// `find -xdev` stops at the config-volume filesystem boundary and won't
// descend into these).
//
// KEEP IN SYNC: if you add/rename/remove a session sub-path, update all
// three places that name it — (1) the mount line here, (2) the DIRS array
// in post-create.sh (so its chown covers the volume), and (3) the mount
// table + Session-resume section in README.md.
//
// Claude: projects/ holds <encoded-cwd>/<uuid>.jsonl transcripts plus the
// sessions-index.json that the `/resume` picker reads. The container cwd is
// always /workspace (encodes to the `-workspace` subdir), so this is the
// container's own slice only. Pure JSONL/JSON — no SQLite/WAL, so a volume
// here is clean. `claude --resume` / `--continue` read straight from it.
"source=${localWorkspaceFolderBasename}-claude-sessions-${devcontainerId},target=/home/node/.claude/projects,type=volume",
// Codex: sessions/ holds YYYY/MM/DD/rollout-*.jsonl transcripts. The thread
// index (state_5.sqlite + -wal/-shm) stays at the ~/.codex root on the
// config volume — it is a single WAL file we must NOT split onto a host
// bind. On a recreation that drops the config volume, that index is cleanly
// absent and Codex rebuilds it from these rollout files on the next start
// (backfill). See README for the one-time-rebuild and corruption caveats.
"source=${localWorkspaceFolderBasename}-codex-sessions-${devcontainerId},target=/home/node/.codex/sessions,type=volume",
// Cursor: chats/{hash}/{uuid}/store.db is one SQLite db per session, each in
// its own leaf dir — a DIRECTORY volume keeps each db beside its -wal/-shm
// sidecar, so there is no cross-filesystem single-file hazard. projects/
// (agent-transcripts) is added too. cursor-agent's on-disk layout is
// community-reverse-engineered (LOW confidence), so this is best-effort;
// keeping it container-private means a wrong guess can't corrupt host state.
"source=${localWorkspaceFolderBasename}-cursor-sessions-${devcontainerId},target=/home/node/.cursor/chats,type=volume",
"source=${localWorkspaceFolderBasename}-cursor-projects-${devcontainerId},target=/home/node/.cursor/projects,type=volume",
//
// OPT-IN: host-shared sessions (like the plugin/skill binds). Uncomment to
// put transcripts on the host — fully visible and portable, but they then
// land on host disk and become a write-through surface for a compromised
// in-container dependency, and the whole-dir binds expose OTHER projects'
// transcripts to the container. Claude is scoped to /workspace's encoded
// subdir to limit that leak; Codex/Cursor stores are not project-scoped, so
// they expose every project. If you enable these, also add the matching
// source dirs to ensure-host-config-dirs.cjs — to its DIRS array (these are
// directory binds), not FILES (which is only for single-file bind sources
// like ~/.claude.json) — so Docker can resolve the binds. Read README
// § "Session resume across recreation" first.
// "source=${localEnv:HOME}/.claude/projects/-workspace,target=/home/node/.claude/projects/-workspace,type=bind",
// "source=${localEnv:HOME}/.codex/sessions,target=/home/node/.codex/sessions,type=bind",
// "source=${localEnv:HOME}/.cursor/chats,target=/home/node/.cursor/chats,type=bind",
// "source=${localEnv:HOME}/.cursor/projects,target=/home/node/.cursor/projects,type=bind",
// Read-only host stage that post-create.sh copies FROM on container-create.
// It is read-only so a container process can never write back to host CLI
// state — that write-back is the attack vector we block. post-create.sh
// reads two kinds of thing from here: (a) the credential + identity files
// (copied into the volume always), and (b) the shareable dirs — skills,
// agents, plugins, memory, commands, prompts, rules — which it copies into
// the volume ONCE on first create (see step 3/4). Nothing here is bound
// read-write into the container, so the host's on-disk setup is protected.
"source=${localEnv:HOME}/.claude,target=/host/.claude,type=bind,readonly",
"source=${localEnv:HOME}/.codex,target=/host/.codex,type=bind,readonly",
"source=${localEnv:HOME}/.cursor,target=/host/.cursor,type=bind,readonly",
// Read-only host stage for the claude-mem store. post-create.sh COPIES it
// into the claude-mem named volume on first create (seed-once). Read-only so
// the container can never write back to the host's live DB — the seed is a
// one-way snapshot. ensure-host-config-dirs.cjs creates ~/.claude-mem on the
// host so this bind resolves even when claude-mem was never installed there.
"source=${localEnv:HOME}/.claude-mem,target=/host/.claude-mem,type=bind,readonly",
// NO read-write bind mounts for the shareable subfolders. They USED to be
// bound here (Claude skills/agents/memory/commands/plugins; Codex plugins/
// prompts/memories/skills; Cursor rules/commands/agents/skills/plugins) so
// host and container shared one copy both ways. That bind was a write-through
// hole: a compromised in-container dependency could drop a malicious skill,
// agent, command, or plugin straight onto the host, which the next HOST
// session would auto-load. To protect the host's on-disk setup, these are
// now COPIED once from the read-only /host/.<cli> stage into the per-container
// named volume by post-create.sh (step 3/4), exactly like claude-mem and the
// session volumes. Trade-offs of the copy model:
// - The container gets its OWN writable copy and can never write back to
// the host. Host setup is protected.
// - It is seed-ONCE: host edits made after first create don't reach the
// container until you remove the config volume and rebuild. Container
// edits persist across rebuilds. (See README § Rebuild/reset to re-seed.)
// - The plugin REGISTRY JSONs (Claude known_marketplaces.json /
// installed_plugins.json / plugin-catalog-cache.json; Cursor
// installed_plugins.json) carry absolute OS-native paths, so they can't
// be copied verbatim — post-create.sh translates their paths to the
// container's Linux paths, also seed-once, alongside the cache/ copy so
// the two stay consistent. Codex needs no translation (config.toml holds
// git URLs, not paths), so its whole plugins/ dir is copied as-is.
// - The old read-only-stage-plus-symlink design failed `/plugin marketplace
// add` in the container with EROFS; copy-into-a-writable-volume avoids
// that — the container writes to its own copy, not a read-only mount.
//
// SINGLE-FILE binds for settings.json, .claude.json, and config.toml are
// deliberately ABSENT. On Docker Desktop Windows the named volume sits on
// one filesystem (ext4, /dev/sdd) and a single-file bind from the host sits
// on another (the 9p drvfs share). Apps save a config by writing `foo.tmp`
// and renaming it over `foo`. That rename can't cross filesystems: it hits
// the EXDEV error and fails with `Device or resource busy` or `inter-device
// move failed`. Codex's TUI shows this as "config/batchWrite failed in
// TUI"; Claude just silently loses the write the same way. Instead, we use
// a read-only host stage at /host/.claude, and post-create.sh copies these
// files into the named volume on every container-create. Host changes show
// up on the next rebuild. Container changes stay inside the container until
// a rebuild.
"source=${localEnv:HOME}/.claude.json,target=/host/.claude.json,type=bind,readonly",
"source=${localEnv:HOME}/.config/git,target=/home/node/.config/git,type=bind,readonly",
"source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,readonly",
// gh uses the COPY-INTO-VOLUME model (read-only host stage at
// /host/.config/gh + the gh-config named volume above). post-create.sh seeds
// hosts.yml/config.yml from this stage into the volume on create, so the
// container has a writable copy: an in-container `gh auth login` persists
// across rebuilds, and nothing is ever written back to the host because this
// stage is read-only. docker stays a direct READ-ONLY bind: the container
// reads your EXISTING host login (the common case), and a compromised
// in-container dependency can't rewrite ~/.docker/config.json (the registry
// credHelper, which points at a binary). A `docker login` run inside the
// container won't persist back to the host — re-run it on the host, or give
// docker the same copy-into-volume treatment as gh. See README § Trust boundary.
"source=${localEnv:HOME}/.config/gh,target=/host/.config/gh,type=bind,readonly",
"source=${localEnv:HOME}/.docker,target=/home/node/.docker,type=bind,readonly",
"source=${localEnv:HOME}/.aws,target=/home/node/.aws,type=bind,readonly",
"source=${localEnv:HOME}/.azure,target=/home/node/.azure,type=bind,readonly",
"source=commandhistory-${devcontainerId},target=/commandhistory,type=volume",
"source=npm-cache-${devcontainerId},target=/home/node/.npm,type=volume",
"source=${localWorkspaceFolderBasename}-root-node-modules-${devcontainerId},target=/workspace/node_modules,type=volume",
"source=${localWorkspaceFolderBasename}-gitnexus-node-modules-${devcontainerId},target=/workspace/gitnexus/node_modules,type=volume",
"source=${localWorkspaceFolderBasename}-gitnexus-web-node-modules-${devcontainerId},target=/workspace/gitnexus-web/node_modules,type=volume",
"source=${localWorkspaceFolderBasename}-gitnexus-shared-node-modules-${devcontainerId},target=/workspace/gitnexus-shared/node_modules,type=volume"
],
// Interactive login is the default way to authenticate for all three CLIs.
// Credentials live in the per-container named volumes (claude-config,
// codex-config, cursor-config), NOT in the host bind mounts. They are copied
// from the read-only /host/.<cli> stage into the volume on container-create.
// Single-file binds would break on Docker Desktop Windows (the EXDEV error).
// Shareable content (plugins, skills, agents, memory, commands) is NOT bound
// read-write — it is copied once from the read-only /host/.<cli> stage into
// the volume on first create, so the host's on-disk setup stays protected.
// API keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, CURSOR_API_KEY) are NOT
// injected via containerEnv. `${localEnv:VAR}` turns an unset host var into
// an empty string. Cursor in particular treats `CURSOR_API_KEY=""` as "use
// this empty key" instead of "fall back to the stored login", which would
// silently break `cursor-agent login`. If you need API-key auth, `export`
// the var in your container shell, or carry it in your VS Code dotfiles repo
// (see .devcontainer/README.md).
// CLAUDE_CONFIG_DIR is left unset on purpose. The Claude default is
// `$HOME/.claude` (= `/home/node/.claude`), which is exactly where the
// claude-config named volume mounts. Setting the env var would change which
// file Claude reads `hasCompletedOnboarding` from. With the var set, Claude
// reads `$CLAUDE_CONFIG_DIR/.claude.json`, the small identity file. Without
// it, Claude reads `$HOME/.claude.json`, the big onboarding-state file that
// actually holds `hasCompletedOnboarding`, the user-scope MCP config, and
// per-project trust. Leaving the var unset matches host behavior. It also
// lets post-create.sh's sync of `$HOME/.claude.json` skip the setup wizard
// on every container-create.
//
// CODEX_HOME is kept even though it matches the Codex default, as a canary.
// If we ever move the Codex named volume target, this env var makes the
// dependency explicit instead of silently following the default.
"containerEnv": {
"CODEX_HOME": "/home/node/.codex",
"DISABLE_AUTOUPDATER": "1",
// post-create.sh removes `installMethod` from the seeded ~/.claude.json so
// the npm-global binary detects its own install method. This is a backup
// safeguard for Claude Code issue #17289. The install-checks routine probes
// ~/.local/bin/claude just because that directory EXISTS. It does exist
// here, because Cursor drops agent and cursor-agent symlinks there. So even
// when installMethod is non-native, the routine reports a false "claude
// command not found at ~/.local/bin/claude". DISABLE_AUTOUPDATER does NOT
// turn that routine off. DISABLE_INSTALLATION_CHECKS is its dedicated kill
// switch.
"DISABLE_INSTALLATION_CHECKS": "1",
"HISTFILE": "/commandhistory/.zsh_history"
},
"customizations": {
"vscode": {
"extensions": [
"anthropic.claude-code",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"eamodio.gitlens"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"files.eol": "\n",
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"bash": { "path": "bash", "icon": "terminal-bash" },
"zsh": { "path": "zsh" }
}
}
}
},
// Do not remap port 4747 (gitnexus serve). gitnexus-web hardcodes
// http://localhost:4747 as its default backend URL.
"forwardPorts": [5173, 4747, 4173],
"portsAttributes": {
"5173": {
"label": "Vite dev (gitnexus-web)",
"onAutoForward": "notify"
},
"4747": {
"label": "gitnexus serve HTTP API",
"onAutoForward": "notify",
"requireLocalPort": true
},
"4173": {
"label": "Static web (Vite preview)",
"onAutoForward": "silent"
}
},
// Lifecycle split (from the Dev Container spec):
// - `updateContentCommand` runs on container-create AND whenever the
// workspace content changes, such as a lockfile update. It owns installing
// the workspace dependencies. Re-installing on every container-create
// wastes time when nothing changed, but it must re-run when deps change.
// - `postCreateCommand` runs once on container-create. It owns syncing the
// AI CLI credentials and identity from the host. That work should happen
// exactly once per container instance, not on every content update.
// Run both with an explicit `bash` so they don't depend on the script's
// executable bit surviving the workspace bind mount.
"updateContentCommand": "bash .devcontainer/install-deps.sh",
"postCreateCommand": "bash .devcontainer/post-create.sh"
}
+149
View File
@@ -0,0 +1,149 @@
// This runs on the HOST, not inside the container, before the dev container is
// created. devcontainer.json calls it via `initializeCommand`. Its job is to
// make sure the bind-mount source folders listed in devcontainer.json already
// exist on the host. Docker rejects a bind mount when its source is missing,
// which happens if a CLI has never been used.
//
// It works on every platform. `os.homedir()` returns the home folder ($HOME on
// Mac/Linux, %USERPROFILE% on Windows). `fs.mkdirSync({recursive: true})`
// creates folders. It is safe to run repeatedly: a path that already exists is
// left alone. We deliberately do NOT handle `~/.gitconfig` here. VS Code's Dev
// Containers extension copies the host gitconfig into the container when you
// attach, and a bind mount fights with that, so it was removed.
//
// The path-creating logic is exported (ensurePaths/DIRS/FILES) so tests can use
// it. The Windows HOME side effect only runs when this file is run directly as
// the initializeCommand. That keeps tests able to drive it against a temp dir
// without touching the real home or calling `setx`.
//
// Host prerequisite: Node.js must be on PATH. That is the only host requirement
// beyond Docker Desktop and the VS Code Dev Containers extension. Everything
// else runs inside the container.
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
// Folders that are bind-mount sources in devcontainer.json. Docker rejects a
// bind mount whose source is missing, so we create each one.
//
// We create the TOP per-CLI folders (~/.claude, ~/.codex, ~/.cursor) and
// ~/.claude-mem. These back the /host/.<cli> and /host/.claude-mem read-only
// STAGE mounts that post-create.sh copies from on container-create. We do NOT
// create the shareable subfolders (skills/agents/plugins/memory/commands/...)
// here anymore: they used to be read-write bind sources, but they are now
// copied once out of the read-only stage into the per-container volume, so they
// are no longer bind sources and pre-creating empty ones would needlessly write
// into the host of someone who never used that CLI. post-create.sh's seed step
// simply skips any subfolder the host doesn't have. The read-only stage bind is
// the whole ~/.<cli> dir, so whatever shareable subfolders DO exist are visible
// to the seed without being listed here.
const DIRS = [
'.claude',
// claude-mem store ($HOME/.claude-mem). A SEPARATE top-level folder from
// ~/.claude, holding claude-mem's SQLite DB + Chroma vector store. It is NOT
// bind-mounted (a multi-GB SQLite/WAL store is unsafe over a 9p bind on Docker
// Desktop Windows). post-create.sh SEEDS it once into a per-container named
// volume from the /host/.claude-mem read-only stage. We create the source here
// so that stage bind resolves even for a host that never ran claude-mem
// (Docker rejects a missing bind source); the seed then finds no DB to copy
// and the container starts with empty memory.
'.claude-mem',
'.codex',
'.cursor',
'.ssh',
'.docker',
'.aws',
'.azure',
path.join('.config', 'gh'),
path.join('.config', 'git'),
];
// Files to pre-create. Only `~/.claude.json` is created here. It is the one
// source that is bound as a single file (read-only at /host/.claude.json). If
// that source is missing, Docker would create a FOLDER in its place, so it has
// to exist as a file first. `~/.claude/settings.json` and
// `~/.codex/config.toml` are NOT single-file binds. post-create.sh copies them
// out of the /host/.<cli> read-only folder stage, and `sync_from_host` simply
// does nothing when they are absent (the `[ -f ]` guard). Creating them here
// would needlessly write to the host of someone who never ran that CLI, so we
// don't.
const FILES = ['.claude.json'];
// Create every folder and touch every file under `home`. Safe to run again:
// an existing path is left untouched. The root is a parameter so tests can run
// it against a temp dir.
function ensurePaths(home, dirs = DIRS, files = FILES) {
for (const dir of dirs) {
const full = path.join(home, dir);
if (!fs.existsSync(full)) {
fs.mkdirSync(full, { recursive: true });
}
}
for (const file of files) {
const full = path.join(home, file);
if (!fs.existsSync(full)) {
fs.closeSync(fs.openSync(full, 'a'));
}
}
}
module.exports = { ensurePaths, DIRS, FILES };
if (require.main === module) {
// One-time setup for native Windows. VS Code fills in the bind-mount sources
// using `${localEnv:HOME}`, which reads its own process environment. Windows
// does not set `HOME` by default; it uses `USERPROFILE`. With no `HOME`, the
// bind sources shrink to filesystem-root paths (`/.claude`, `/.codex`, ...)
// and Docker rejects them with `bind source path does not exist`.
//
// The fix is to save `HOME=%USERPROFILE%` into the user's environment with
// `setx`. `setx` writes to `HKCU\Environment`. Every process the user starts
// after that inherits the new value, including VS Code once it restarts. The
// current VS Code process can't see the change, because its environment was
// set when it launched. So we tell the user to restart VS Code once.
//
// Later runs see that `HOME` is set, skip this block, and continue normally.
// Mac, Linux, and WSL hosts already have `HOME` set by the shell, so this
// block does nothing on those platforms.
if (process.platform === 'win32' && !process.env.HOME) {
const userprofile = process.env.USERPROFILE;
if (userprofile) {
try {
require('child_process').execFileSync('setx', ['HOME', userprofile], {
stdio: 'ignore',
});
console.error('');
console.error('='.repeat(70));
console.error(' GitNexus devcontainer one-time Windows setup');
console.error('='.repeat(70));
console.error('');
console.error(`HOME has been set to %USERPROFILE% (${userprofile}).`);
console.error("VS Code reads this at startup, so the current session can't pick it up.");
console.error('');
console.error(' 1. Close ALL VS Code windows (File > Exit, not just the window).');
console.error(' 2. Reopen VS Code, open this folder, and re-run Reopen in Container.');
console.error('');
console.error('This is a one-time setup. Subsequent rebuilds work normally.');
console.error('='.repeat(70));
process.exit(1);
} catch (err) {
console.error('ERROR: failed to set HOME automatically: ' + err.message);
console.error('');
console.error('Run this in a Windows shell, then restart VS Code:');
console.error(' setx HOME "%USERPROFILE%"');
process.exit(1);
}
} else {
console.error('ERROR: neither HOME nor USERPROFILE is set on this host.');
console.error('');
console.error('Set HOME to your user profile directory and restart VS Code:');
console.error(' setx HOME "%USERPROFILE%"');
process.exit(1);
}
}
ensurePaths(os.homedir());
}
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Devcontainer updateContentCommand. The Dev Container spec runs this when the
# container is created AND whenever workspace content changes (for example a
# lockfile update). This script installs workspace dependencies only. Syncing AI
# CLI state lives in post-create.sh, which runs once right after this.
#
# Why the split: updateContentCommand re-runs on content changes, but
# postCreateCommand runs only at container-create. Keeping `npm install` here
# means a rebuild after pulling new dependencies refreshes them. The AI CLI
# credential and path-translation work does not re-run each time.
set -euo pipefail
cd /workspace
echo "[install-deps] 1/4: chown workspace node_modules + npm cache mount points"
# The named volumes (workspace/*/node_modules and ~/.npm) are created at first
# mount. They inherit ownership from the image's UID before realignment. Then
# `updateRemoteUserUID: true` shifts the `node` user's UID. Now the volumes are
# owned by the old, stale UID and npm install cannot write to them. So we chown
# again here, after realignment. Running it again later changes nothing.
#
# We use `find -xdev -exec chown -h` (the same idiom as post-create.sh) instead
# of a plain `chown -R`. There are two separate guards. First, `-xdev` stops
# find from descending past each volume's own filesystem, so it won't recurse
# into a host folder mounted underneath. Second, `-h` makes chown change the
# symlink itself instead of following it to its target. Without `-h`, a symlink
# in the tree (one a dependency's postinstall drops, or a dangling
# node_modules/.bin link) would either send the chown onto a target on another
# filesystem, or fail to follow and abort the whole script under `set -e`. For
# regular files and directories `-h` does nothing, so the ownership fix is the
# same.
for d in /workspace/node_modules \
/workspace/gitnexus/node_modules \
/workspace/gitnexus-web/node_modules \
/workspace/gitnexus-shared/node_modules \
/home/node/.npm; do
sudo find "$d" -xdev -exec chown -h node:node {} +
done
echo "[install-deps] 2/4: clear stale .husky/_ runtime cache"
# On Docker Desktop for Windows, the bind-mount permission translation won't let
# the new container's `node` user overwrite a `.husky/_/h` file that an earlier
# container wrote under a different UID. So we delete it. `.husky/_` is a
# gitignored runtime cache, and husky rebuilds it during the root `npm install`.
# Husky upstream has no fix for this UID clash.
rm -rf .husky/_
echo "[install-deps] 3/4: npm install at root, then gitnexus-shared (build required)"
# Install order matters. Root goes first, for lint-staged, husky, and prettier.
# Then gitnexus-shared, which must be built before installing gitnexus-web or
# gitnexus. Both of those depend on it via `file:../gitnexus-shared`.
npm install
cd /workspace/gitnexus-shared
npm install
npm run build
echo "[install-deps] 4/4: npm install gitnexus-web, then gitnexus"
# gitnexus-web goes before gitnexus. The gitnexus `prepare` script runs
# scripts/build.js, which compiles gitnexus-web when that directory is present.
# In the devcontainer the whole workspace is bind-mounted, so gitnexus-web/ is
# present when gitnexus installs. The production Dockerfiles COPY only selected
# files, so the directory is not present there.
cd /workspace/gitnexus-web
npm install
cd /workspace/gitnexus
npm install
echo "[install-deps] done"
+310
View File
@@ -0,0 +1,310 @@
#!/usr/bin/env bash
# Devcontainer postCreate script. It runs once, right after the container is
# created. devcontainer.json wires it up via `postCreateCommand`. Workspace
# dependencies are installed elsewhere, in install-deps.sh (`updateContentCommand`).
# That script runs BEFORE this one — that is the order the devcontainer spec
# defines. This script does one job: sync the AI CLI credentials and identity
# from the host.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[post-create] 1/4: chown AI CLI named-volume mount points"
# Fix ownership on the named volumes (~/.claude, ~/.codex, ~/.cursor,
# /commandhistory). When they first mount, they take the user ID baked into the
# image, before any realignment. Then `updateRemoteUserUID: true` shifts the
# `node` user to a new ID. Now the volumes are owned by the old, stale ID, and
# writes into them fail. (~/.local is a directory in the image, not a volume.
# We chown it too, just to be safe.) install-deps.sh fixes the workspace side.
# This script fixes the AI CLI side, so each lifecycle hook handles its own part.
#
# There are two separate guards here, and they do different things. `-xdev`
# keeps find from descending into other filesystems. The shareable dirs (skills,
# agents, plugins, memory, commands, prompts, rules) are no longer host bind
# mounts — they now live INSIDE the config volume (seeded in step 3/4), so
# `-xdev` correctly walks and chowns them as the container-private volume files
# they are. What `-xdev` still stops at are the SESSION volumes (mount group 6),
# which remain separate filesystems mounted at sub-paths (see below). `-h` tells
# chown to act on a symlink ITSELF instead of following it, so it never lands on
# a target across a filesystem boundary and never aborts on a broken symlink
# under `set -e` (a legacy Option-B symlink could still exist on a carried-over
# volume). For regular files and directories `-h` does nothing extra.
#
# The session volumes (mount group 6: .claude/projects, .codex/sessions,
# .cursor/chats, .cursor/projects) are their OWN filesystems mounted at
# sub-paths, so `-xdev` rooted at the config-volume parent deliberately skips
# them. That is why each one is listed as its own root below: rooted there,
# `-xdev` walks just that volume and chowns its top level, so the CLI's first
# write doesn't hit EACCES on a stale image UID. These are container-private
# volumes, not the host's own files — the read-only /host/.<cli> stages we copy
# from are mounted elsewhere and are never chowned.
DIRS=(
/home/node/.claude
/home/node/.claude/projects
/home/node/.codex
/home/node/.codex/sessions
/home/node/.cursor
/home/node/.cursor/chats
/home/node/.cursor/projects
/home/node/.config/gh
/home/node/.local
/commandhistory
)
# claude-mem volume: chown it ONLY on first create (its completion sentinel is
# absent). The step-4/4 seed copies the store as the node user, so a populated
# claude-mem volume is already node-owned on every later rebuild — a recursive
# `find` over a multi-GB store (the 7GB+ DB plus the Chroma index) just to
# re-stamp ownership that is already correct would add real latency to every
# rebuild for nothing. On first create the volume is empty, so this chown of the
# bare mount point is trivial and lets the seed write into it.
[ -f /home/node/.claude-mem/.claude-mem-seeded ] || DIRS+=(/home/node/.claude-mem)
for d in "${DIRS[@]}"; do
# Skip a root that isn't present rather than aborting the whole run under
# `set -e`. Docker creates every declared volume's mount point before this
# script runs, so in the normal case all roots exist and this is a no-op.
# The guard matters only if a session volume is later removed from
# devcontainer.json without its matching DIRS entry being removed too — then
# provisioning skips it instead of failing before credentials ever sync.
[ -d "$d" ] || continue
sudo find "$d" -xdev -exec chown -h node:node {} +
done
echo "[post-create] 2/4: sync AI CLI credentials + identity from host"
# Clean up after an older devcontainer design (Option B). Back then these paths
# were symlinks pointing into the read-only host stage
# (e.g. /home/node/.claude/plugins -> /host/.claude/plugins). A write through
# such a symlink would land on a read-only host file and fail. Delete any that
# survive on a carried-over volume. The shareable dirs are now real directories
# in the named volume, seeded from the host in step 3/4 below.
for p in plugins skills agents memory commands; do
[ -L "/home/node/.claude/$p" ] && rm "/home/node/.claude/$p"
done
for p in plugins prompts memories skills config.toml; do
[ -L "/home/node/.codex/$p" ] && rm "/home/node/.codex/$p"
done
for p in plugins rules commands agents skills; do
[ -L "/home/node/.cursor/$p" ] && rm "/home/node/.cursor/$p"
done
mkdir -p /home/node/.claude/plugins /home/node/.cursor/plugins
# Shareable content (skills, agents, plugins, memory, commands, prompts, rules)
# is NO LONGER bind-mounted. It is COPIED once from the read-only host stage into
# the named volume in step 3/4 below, so a compromised in-container dependency
# can't write through to the host's on-disk CLI setup. This step handles only the
# credentials, identity, and single config files. Those stay per-container in the
# named volume and are COPIED from the host once when the container is created:
# - .credentials.json (Claude OAuth tokens)
# - .claude/.claude.json (Claude identity: userID, oauthAccount, and
# migration tracking — a different file from $HOME/.claude.json)
# - settings.json (Claude), config.toml (Codex), mcp.json (Cursor). These are
# single config files, and single files can't be bind-mounted on Windows
# (the EXDEV error explained below).
# - auth.json (Codex), cli-config.json (Cursor — which mixes auth and settings)
# - the plugin registry JSONs that contain absolute paths (Claude + Cursor).
# Those are translated below.
#
# How the sync behaves: it ALWAYS overwrites from the host when the container is
# created. A fresh container then starts logged in as the host's user, if the
# host had credentials. From that point the container manages its own login,
# until the next rebuild copies the host files again. Logging out inside the
# container does NOT log out the host. Per-container login is the goal, and
# bind-mounting these files would instead make a logout shared between both.
sync_from_host() {
local src=$1
local dst=$2
local mode=${3:-600}
if [ -f "$src" ]; then
rm -f "$dst"
cp "$src" "$dst"
chmod "$mode" "$dst"
fi
}
sync_from_host \
/host/.claude/.credentials.json /home/node/.claude/.credentials.json
sync_from_host \
/host/.claude/.claude.json /home/node/.claude/.claude.json 644
# These config files are COPIED from the host, not bind-mounted. We tried
# bind-mounting them as single files and it didn't work. On Docker Desktop for
# Windows the named volume (ext4) and the host bind mount (9p drvfs) are
# different filesystems. Apps save a config by writing a temp file and renaming
# it over the real one, and that rename fails across filesystems (the "EXDEV" or
# "Device or resource busy" error). So copy the host's version into the named
# volume when the container is created. The container can then rewrite it freely
# until the next rebuild copies the host version again.
sync_from_host /host/.claude/settings.json /home/node/.claude/settings.json 644
sync_from_host /host/.codex/config.toml /home/node/.codex/config.toml 644
# Seed $HOME/.claude.json from the host, but NOT as a straight copy. That file
# mixes two kinds of state. Some is portable account and onboarding state we
# want to keep: hasCompletedOnboarding, oauthAccount, userID, projects,
# tipsHistory. The rest describes how Claude is installed on the host, and that
# part is never valid here. This image installs Claude with `npm install -g`,
# but the host's `installMethod` (for example "native") makes Claude look for
# ~/.local/bin/claude and fail with
# "claude command not found at /home/node/.local/bin/claude". The fix strips the
# machine-specific fields and forces hasCompletedOnboarding, while handling a
# host file that isn't a JSON object. That logic lives in seed-claude-config.cjs
# so it can be unit-tested and prettier-checked
# (translate-plugin-registries.test.cjs).
node "$SCRIPT_DIR/seed-claude-config.cjs"
# Codex auth. Some hosts store credentials in the OS keyring instead of on disk
# (`cli_auth_credentials_store = "keyring"`, the default on macOS). Those hosts
# have no auth.json file, so the copy below quietly does nothing. In that case,
# log in inside the container with `codex login --device-auth`.
sync_from_host \
/host/.codex/auth.json /home/node/.codex/auth.json
# Cursor CLI. Its cli-config.json holds both auth and settings in one file.
# Cursor has known upstream problems authenticating inside Docker, even when the
# config is copied correctly. If `cursor-agent` reports auth errors after the
# copy, run `cursor-agent login` again inside the container. mcp.json (Cursor's
# MCP server config) is also a single file, so it is copied on create rather
# than bind-mounted, for the same EXDEV reason as above. hooks.json is left out
# on purpose. Cursor hooks run shell commands, and sharing the host's hooks
# would widen the supply-chain attack surface inside the container. Copy it in
# yourself if you want the host's hooks in the container.
sync_from_host \
/host/.cursor/cli-config.json /home/node/.cursor/cli-config.json
sync_from_host \
/host/.cursor/mcp.json /home/node/.cursor/mcp.json 644
# gh CLI auth + settings. Same copy-into-volume model as the credentials above:
# hosts.yml holds the GitHub token (mode 600), config.yml holds settings (644).
# Copied from the read-only /host/.config/gh stage into the gh-config named
# volume on create. Because the volume is writable, an in-container
# `gh auth login` / `gh auth refresh` persists across rebuilds; because the
# stage is read-only, nothing flows back to the host. If the host had no login,
# both copies quietly no-op and whatever the container wrote is kept.
sync_from_host /host/.config/gh/hosts.yml /home/node/.config/gh/hosts.yml
sync_from_host /host/.config/gh/config.yml /home/node/.config/gh/config.yml 644
echo "[post-create] 3/4: seed shareable config dirs from host (first create only)"
# The shareable dirs (Claude skills/agents/memory/commands/plugins; Codex
# plugins/prompts/memories/skills; Cursor rules/commands/agents/skills/plugins)
# used to be read-write host bind mounts, so a write inside the container landed
# directly on the host's files. That exposed the host's on-disk CLI setup: a
# compromised workspace dependency running in the container could drop a malicious
# skill, agent, command, or plugin into the host's folders, which the next HOST
# session would then auto-load. To protect the host, these are no longer bound.
# Instead we COPY them once from the read-only /host/.<cli> stage into the
# per-container named volume, exactly like claude-mem (step 4/4) and the session
# volumes. The container gets its own writable copy and can NEVER write back to
# the host. The container also avoids the old read-only-stage EROFS failure,
# because it writes to its own volume copy, not a read-only mount.
#
# Seed-once, persist: a per-CLI marker file records that the copy has happened.
# On the first container-create the marker is absent, so we copy; on every later
# rebuild the marker is present, so we skip and keep whatever the container has
# accumulated. Host edits made AFTER the first create do NOT reach the container
# until you remove the config volume and rebuild (see README § Rebuild/reset).
seed_shareable() {
# seed_shareable <cli> <subdir>...: copy each /host/.<cli>/<subdir> into the
# named volume, once. Skips a subdir the host doesn't have. We use `cp -r`,
# NOT `cp -a`/`cp -p`: this script runs as the non-root node user, and the
# host-stage files are owned by a different UID, so trying to preserve
# ownership would fail with EPERM and abort the run under `set -e` (the same
# reason sync_from_host uses plain cp). `cp -r` copies contents owned by node
# — exactly what we want — and preserves symlinks as symlinks (GNU default).
local cli=$1
shift
local marker="/home/node/.$cli/.devcontainer-shareable-seeded"
[ -f "$marker" ] && return 0
for sub in "$@"; do
local src="/host/.$cli/$sub"
local dst="/home/node/.$cli/$sub"
[ -d "$src" ] || continue
mkdir -p "$dst"
cp -r "$src/." "$dst/"
done
}
# Decide which plugin registries to translate BEFORE seeding sets the markers.
# We translate only a CLI being seeded this run, so a plugin installed inside the
# container isn't overwritten by the host's registry on a later rebuild. Codex
# has no path-bearing registry (config.toml holds git URLs), so it's never here.
TRANSLATE_CLIS=()
[ -f /home/node/.claude/.devcontainer-shareable-seeded ] || TRANSLATE_CLIS+=(claude)
[ -f /home/node/.cursor/.devcontainer-shareable-seeded ] || TRANSLATE_CLIS+=(cursor)
seed_shareable claude skills agents memory commands plugins/marketplaces plugins/cache
seed_shareable codex plugins prompts memories skills
seed_shareable cursor rules commands agents skills plugins/marketplaces plugins/local
# Translate the path-bearing plugin registries (Claude + Cursor) for the CLIs we
# just seeded. They store absolute, OS-native install paths
# (`C:\Users\X\.claude\plugins\...` on Windows), which the Linux container can't
# resolve — it would fail with `cache-miss`. translate-plugin-registries.cjs
# rewrites those to the container's paths and writes the result into the volume.
if [ "${#TRANSLATE_CLIS[@]}" -gt 0 ]; then
node "$SCRIPT_DIR/translate-plugin-registries.cjs" "${TRANSLATE_CLIS[@]}"
fi
# Record that each CLI's shareable surface is seeded, so later rebuilds keep the
# container's copy. Touch even when the host had nothing to copy — an empty CLI
# is still "seeded", and we don't want to re-scan the host on every rebuild.
#
# ORDERING INVARIANT — do NOT move these touches earlier (e.g. into
# seed_shareable per-CLI). The markers must be written only AFTER the registry
# translation above, because seed (cache copy) and translate (registry rewrite)
# are logically atomic: a marker set between them would let a later rebuild skip
# translation for an already-seeded CLI, leaving its cache/ in place but its
# registry still pointing at host paths (`cache-miss`). Writing all markers here,
# after translate, means any abort mid-seed leaves NO markers, so the next create
# re-runs the whole seed+translate. The cost is re-copying an already-copied CLI
# on retry; `cp -r` overwrites in place, so that is idempotent and cheap relative
# to a broken plugin registry.
for cli in claude codex cursor; do
touch "/home/node/.$cli/.devcontainer-shareable-seeded"
done
echo "[post-create] 4/4: seed claude-mem store from host (first create only)"
# claude-mem keeps its memory in $HOME/.claude-mem — a SQLite DB (claude-mem.db
# plus -wal/-shm) and a Chroma vector store (chroma/chroma.sqlite3 + HNSW index
# binaries). It is mounted as a per-container named volume, NOT a host bind:
# pushing a multi-GB SQLite/WAL store over the 9p/virtiofs bind risks unreliable
# fcntl locking and corruption, especially if claude-mem ran on the host and in
# the container against the same files at once (see devcontainer.json).
#
# So seed it ONCE, then let the container own its copy. On every later rebuild
# we skip the copy and keep whatever the container has accumulated since —
# rebuilds never clobber it. The container's memory and the host's diverge from
# this seed point on; that is the deliberate cost of keeping SQLite off a shared
# bind. To re-seed from the host, remove the volume (`docker volume rm
# claude-mem-<id>`) and rebuild.
#
# The skip guard is a COMPLETION SENTINEL (.claude-mem-seeded), NOT the presence
# of claude-mem.db. Keying on the DB file would be a trap: a multi-GB `cp -r` can
# be interrupted (disk full, I/O error) and abort the script under `set -e`,
# leaving a PARTIAL claude-mem.db behind. The next create would then see that
# truncated file and treat the store as "already seeded", sticking the container
# with a corrupt DB forever. With a sentinel touched only AFTER `cp` returns 0,
# an interrupted seed leaves no sentinel; the next create clears the half-copied
# store and retries cleanly. CONSISTENCY: copying a live WAL database is only
# crash-consistent if claude-mem is NOT writing on the host during the copy — do
# not run claude-mem on the host during a first-create or a re-seed rebuild.
#
# `cp -r` (not `cp -a`/`cp -p`) copies the DB together with its -wal/-shm
# sidecars in one pass. We avoid preserving ownership for the same reason as the
# shareable seed above: this runs as the non-root node user against host-owned
# files, so `cp -a` would fail with EPERM and abort under `set -e`. `cp -r`
# leaves the copies owned by node. The host stage is read-only, so this can
# never write back to the host's live DB.
if [ -f /host/.claude-mem/claude-mem.db ] && [ ! -f /home/node/.claude-mem/.claude-mem-seeded ]; then
echo "[post-create] seeding ~/.claude-mem from host (one-time copy, may be several GB)"
# Clear any partial store left by a previously-interrupted seed (mindepth 1
# so the volume mount point itself is never removed), then copy and only then
# write the sentinel. A partial store is node-owned (cp runs as node, and
# step 1 re-chowns the volume whenever the sentinel is absent), so no sudo.
find /home/node/.claude-mem -mindepth 1 -maxdepth 1 -exec rm -rf {} +
cp -r /host/.claude-mem/. /home/node/.claude-mem/
touch /home/node/.claude-mem/.claude-mem-seeded
else
echo "[post-create] skipping claude-mem seed (already seeded, or host has no store)"
fi
echo "[post-create] done"
+83
View File
@@ -0,0 +1,83 @@
// Builds the container's $HOME/.claude.json from the host's copy. It does NOT
// copy the host file verbatim. The host's ~/.claude.json holds two kinds of
// data. Some is portable account and onboarding state: hasCompletedOnboarding,
// oauthAccount, userID, projects, tipsHistory. We keep that. The rest tracks
// how Claude was installed on the host machine, and that is never right inside
// this container.
//
// Here is why the install fields break things. The image installs Claude with
// `npm install -g`. But if the host's `installMethod` says something like
// "native", Claude looks for ~/.local/bin/claude and fails with
// "claude command not found at /home/node/.local/bin/claude". So we drop the
// install and machine fields. With them gone, the npm-global binary detects its
// own install method. We also force hasCompletedOnboarding so the setup wizard
// is skipped, even when the host has never run Claude before.
//
// This logic was pulled out of a heredoc in post-create.sh. As its own file the
// transform can be unit-tested and prettier-checked (see seed-claude-config.test
// via the translate-plugin-registries test harness). DISABLE_AUTOUPDATER=1 in
// containerEnv already stops runtime updates. This file only quiets the doctor
// mismatch and the native-path probe.
'use strict';
const fs = require('fs');
// Fields that describe how Claude was installed on the host machine. They are
// never valid in an `npm install -g` container. Removing them lets Claude
// detect the npm-global install on its own.
const MACHINE_FIELDS = [
'installMethod',
'autoUpdates',
'autoUpdatesProtectedForNative',
'shiftEnterKeyBindingInstalled',
];
// Pure transform: take whatever the host file parsed to and return a config
// object suitable for the container. It also guards against a host file that is
// valid JSON but not an object. A bare number, string, or array would pass the
// parse try/catch. Then the field deletes would do nothing, the
// hasCompletedOnboarding assignment would silently fail, and onboarding would
// trigger again on every rebuild. The guard replaces such a value with {}.
function sanitizeClaudeConfig(parsed) {
let cfg = parsed;
if (cfg === null || typeof cfg !== 'object' || Array.isArray(cfg)) {
cfg = {};
}
for (const k of MACHINE_FIELDS) {
delete cfg[k];
}
cfg.hasCompletedOnboarding = true; // skip the wizard, even on a first-time host
return cfg;
}
function readHostConfig(src) {
try {
if (fs.existsSync(src) && fs.statSync(src).size > 0) {
return JSON.parse(fs.readFileSync(src, 'utf8'));
}
} catch {
// Host file is malformed or unreadable. Fall back to an empty config so the
// container still gets a valid file that carries hasCompletedOnboarding.
}
return {};
}
function main() {
const src = process.argv[2] || '/host/.claude.json';
const dst = process.argv[3] || '/home/node/.claude.json';
const cfg = sanitizeClaudeConfig(readHostConfig(src));
try {
fs.writeFileSync(dst, JSON.stringify(cfg, null, 2));
fs.chmodSync(dst, 0o644);
} catch (err) {
console.error(`[post-create] ERROR: failed to seed ${dst}: ${err && err.message}`);
process.exit(1);
}
}
module.exports = { sanitizeClaudeConfig, readHostConfig, MACHINE_FIELDS };
if (require.main === module) {
main();
}
@@ -0,0 +1,107 @@
// Rewrites the host paths inside Claude and Cursor plugin-registry JSON files
// so they point at the container's Linux paths, then writes the results into
// the named volume.
//
// Why: both CLIs store absolute, OS-native install paths in their registry
// JSONs. On Windows that looks like `C:\Users\X\.claude\plugins\...`; on macOS
// like `/Users/X/.cursor/...`. The Linux container can't use those paths. If we
// just bind-mounted the host files in, the CLI would try to resolve a Windows
// path under Linux and fail with `cache-miss`. So for each CLI we read the host
// registry, rewrite every absolute path ending in `/.<cli>/plugins/<rest>` to
// `/home/node/.<cli>/plugins/<rest>`, and write the result into the named volume.
//
// Codex is left alone. Its registry is config.toml and holds git URLs, not
// filesystem paths, so there's nothing to translate — its whole plugins/ dir is
// copied as-is into the container volume instead (seeded once by post-create.sh).
//
// This code lived inside a post-create.sh heredoc. We pulled it out so the regex
// and the deep rewrite can be unit-tested and prettier-checked. The regex has
// had path-handling bugs before.
'use strict';
const fs = require('fs');
const path = require('path');
// Build a regex that matches an absolute path containing
// `<sep>.<cli><sep>plugins<sep><rest>`, where <sep> is `/` or `\`. It's anchored
// at the start of the string. The lazy `.*?` eats the home prefix up to the
// FIRST `.<cli>/plugins` segment.
function buildRe(cliName) {
return new RegExp(`^(?:[A-Za-z]:)?[\\\\/].*?[\\\\/]\\.${cliName}[\\\\/]plugins[\\\\/](.*)$`);
}
// Walk `obj` and rewrite every string value that matches `re`. A match is
// remapped under `ctr`, the container's plugins dir. Windows backslashes in the
// matched part are switched to forward slashes.
function rewriteDeep(obj, re, ctr) {
if (Array.isArray(obj)) return obj.map((v) => rewriteDeep(v, re, ctr));
if (obj && typeof obj === 'object') {
const out = {};
for (const [k, v] of Object.entries(obj)) out[k] = rewriteDeep(v, re, ctr);
return out;
}
if (typeof obj === 'string') {
return obj.replace(re, (_, rest) => `${ctr}/${rest.replace(/\\/g, '/')}`);
}
return obj;
}
const REGISTRIES = [
{
cli: 'claude',
host: '/host/.claude/plugins',
ctr: '/home/node/.claude/plugins',
files: ['known_marketplaces.json', 'installed_plugins.json', 'plugin-catalog-cache.json'],
},
{
cli: 'cursor',
host: '/host/.cursor/plugins',
ctr: '/home/node/.cursor/plugins',
files: ['installed_plugins.json'],
},
];
function translate(registries) {
for (const reg of registries) {
const re = buildRe(reg.cli);
try {
fs.mkdirSync(reg.ctr, { recursive: true });
} catch (err) {
console.error(`[post-create] ERROR: failed to create ${reg.ctr}: ${err && err.message}`);
process.exit(1);
}
for (const name of reg.files) {
const src = path.join(reg.host, name);
const dst = path.join(reg.ctr, name);
if (!fs.existsSync(src) || fs.statSync(src).size === 0) continue;
let data;
try {
data = JSON.parse(fs.readFileSync(src, 'utf8'));
} catch {
continue; // Skip a malformed host registry instead of aborting.
}
try {
fs.writeFileSync(dst, JSON.stringify(rewriteDeep(data, re, reg.ctr), null, 2));
} catch (err) {
console.error(`[post-create] ERROR: failed to write ${dst}: ${err && err.message}`);
process.exit(1);
}
}
}
}
// Filter the registry table by CLI name. post-create.sh passes the CLIs it is
// seeding this run (e.g. `claude`), so a registry is only (re)generated on the
// FIRST container-create for that CLI — never on a rebuild, where it would
// clobber a plugin the user installed inside the container. An empty filter
// (no args) means "translate every registry" — the original behavior.
function selectRegistries(registries, only) {
return only && only.length ? registries.filter((r) => only.includes(r.cli)) : registries;
}
module.exports = { buildRe, rewriteDeep, REGISTRIES, translate, selectRegistries };
if (require.main === module) {
translate(selectRegistries(REGISTRIES, process.argv.slice(2)));
}
@@ -0,0 +1,436 @@
// Unit tests for the devcontainer host->container config transforms.
//
// This code used to live inside post-create.sh heredocs, where lint could not
// see it and tests could not reach it. We test three things:
// - plugin-registry path translation (buildRe + rewriteDeep + the real
// filesystem translate() driver). Path handling here has had bugs before.
// - the strip of machine-specific fields from $HOME/.claude.json
// (sanitizeClaudeConfig + readHostConfig + the seed-claude-config main()
// entry point)
// - the host bind-source bootstrap (ensurePaths). One test guards against a
// regression: ensurePaths must NOT pre-create settings.json / config.toml
// on the host.
//
// We test both pure functions and code that touches the filesystem. The
// filesystem tests use throwaway directories under os.tmpdir() and delete them
// when done. So they run in CI with no mounts and never touch the real home dir.
//
// Run with the built-in Node test runner (no extra dependencies):
// node --test .devcontainer/
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const os = require('node:os');
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const {
buildRe,
rewriteDeep,
translate,
selectRegistries,
} = require('./translate-plugin-registries.cjs');
const { sanitizeClaudeConfig, readHostConfig } = require('./seed-claude-config.cjs');
const { ensurePaths, DIRS, FILES } = require('./ensure-host-config-dirs.cjs');
const CLAUDE = '/home/node/.claude/plugins';
const CURSOR = '/home/node/.cursor/plugins';
// Make a fresh throwaway directory under the OS temp root. mkdtemp picks a
// unique name on every call, so we don't need Date.now() or random names.
function tmp() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gn-dc-'));
}
function rw(value, cli, ctr) {
return rewriteDeep(value, buildRe(cli), ctr);
}
test('claude: Windows backslash absolute path -> container path', () => {
assert.equal(
rw('C:\\Users\\gergo\\.claude\\plugins\\cache\\x\\1.0', 'claude', CLAUDE),
'/home/node/.claude/plugins/cache/x/1.0',
);
});
test('claude: Windows forward-slash absolute path -> container path', () => {
assert.equal(
rw('C:/Users/gergo/.claude/plugins/marketplaces/m', 'claude', CLAUDE),
'/home/node/.claude/plugins/marketplaces/m',
);
});
test('claude: macOS POSIX path -> container path', () => {
assert.equal(
rw('/Users/alice/.claude/plugins/marketplaces/m', 'claude', CLAUDE),
'/home/node/.claude/plugins/marketplaces/m',
);
});
test('claude: Linux POSIX path -> container path', () => {
assert.equal(
rw('/home/bob/.claude/plugins/cache/foo', 'claude', CLAUDE),
'/home/node/.claude/plugins/cache/foo',
);
});
test('cursor: Windows path -> container cursor path', () => {
assert.equal(
rw('C:\\Users\\gergo\\.cursor\\plugins\\local\\myplug', 'cursor', CURSOR),
'/home/node/.cursor/plugins/local/myplug',
);
});
test('cross-CLI isolation: claude regex leaves a .cursor path untouched', () => {
const input = 'C:\\Users\\g\\.cursor\\plugins\\x';
assert.equal(rw(input, 'claude', CLAUDE), input);
});
test('non-path strings pass through unchanged', () => {
assert.equal(rw('not-a-path', 'claude', CLAUDE), 'not-a-path');
assert.equal(
rw('https://github.com/EveryInc/x.git', 'claude', CLAUDE),
'https://github.com/EveryInc/x.git',
);
});
test('non-string scalars pass through unchanged', () => {
assert.equal(rw(42, 'claude', CLAUDE), 42);
assert.equal(rw(null, 'claude', CLAUDE), null);
assert.equal(rw(true, 'claude', CLAUDE), true);
});
test('nested objects/arrays are rewritten deeply', () => {
const input = {
'compound-engineering@m': [
{ installPath: 'C:\\Users\\g\\.claude\\plugins\\cache\\ce\\3.9.2', version: '3.9.2' },
],
nested: { installLocation: '/Users/g/.claude/plugins/marketplaces/m' },
};
const out = rw(input, 'claude', CLAUDE);
assert.equal(
out['compound-engineering@m'][0].installPath,
'/home/node/.claude/plugins/cache/ce/3.9.2',
);
assert.equal(out['compound-engineering@m'][0].version, '3.9.2');
assert.equal(out.nested.installLocation, '/home/node/.claude/plugins/marketplaces/m');
});
test('sanitizeClaudeConfig: strips machine fields, forces hasCompletedOnboarding', () => {
const out = sanitizeClaudeConfig({
installMethod: 'native',
autoUpdates: false,
autoUpdatesProtectedForNative: true,
shiftEnterKeyBindingInstalled: true,
userID: 'abc',
oauthAccount: { emailAddress: 'x@y.z' },
});
assert.equal(out.installMethod, undefined);
assert.equal(out.autoUpdates, undefined);
assert.equal(out.autoUpdatesProtectedForNative, undefined);
assert.equal(out.shiftEnterKeyBindingInstalled, undefined);
assert.equal(out.userID, 'abc');
assert.equal(out.oauthAccount.emailAddress, 'x@y.z');
assert.equal(out.hasCompletedOnboarding, true);
});
test('sanitizeClaudeConfig: non-object inputs become a valid onboarding-bearing object', () => {
for (const bad of [42, 'x', null, ['a'], true]) {
const out = sanitizeClaudeConfig(bad);
assert.equal(typeof out, 'object');
assert.equal(Array.isArray(out), false);
assert.equal(out.hasCompletedOnboarding, true);
}
});
test('sanitizeClaudeConfig: empty object still gets hasCompletedOnboarding', () => {
assert.deepEqual(sanitizeClaudeConfig({}), { hasCompletedOnboarding: true });
});
// --- readHostConfig: reading the file, and the fallbacks when it fails ------
test('readHostConfig: missing file -> {}', () => {
const dir = tmp();
try {
assert.deepEqual(readHostConfig(path.join(dir, 'nope.json')), {});
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('readHostConfig: empty (zero-byte) file -> {}', () => {
const dir = tmp();
try {
const f = path.join(dir, 'empty.json');
fs.writeFileSync(f, '');
assert.deepEqual(readHostConfig(f), {});
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('readHostConfig: malformed JSON -> {}', () => {
const dir = tmp();
try {
const f = path.join(dir, 'bad.json');
fs.writeFileSync(f, '{ not valid json');
assert.deepEqual(readHostConfig(f), {});
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('readHostConfig: valid object is parsed through', () => {
const dir = tmp();
try {
const f = path.join(dir, 'ok.json');
fs.writeFileSync(f, JSON.stringify({ userID: 'u', hasCompletedOnboarding: false }));
const out = readHostConfig(f);
assert.equal(out.userID, 'u');
assert.equal(out.hasCompletedOnboarding, false);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// --- translate(): runs against real registry files on disk ------------------
test('translate: rewrites host absolute paths and writes into the ctr dir', () => {
const hostDir = tmp();
const ctrParent = tmp();
const ctrDir = path.join(ctrParent, 'plugins'); // need not exist yet; translate creates it
try {
const reg = [{ cli: 'claude', host: hostDir, ctr: ctrDir, files: ['installed_plugins.json'] }];
fs.writeFileSync(
path.join(hostDir, 'installed_plugins.json'),
JSON.stringify({ 'p@m': [{ installPath: 'C:\\Users\\g\\.claude\\plugins\\cache\\p\\1.0' }] }),
);
translate(reg);
const out = JSON.parse(fs.readFileSync(path.join(ctrDir, 'installed_plugins.json'), 'utf8'));
assert.equal(out['p@m'][0].installPath, `${ctrDir}/cache/p/1.0`);
} finally {
fs.rmSync(hostDir, { recursive: true, force: true });
fs.rmSync(ctrParent, { recursive: true, force: true });
}
});
test('translate: idempotent — a second run reproduces byte-identical output', () => {
const hostDir = tmp();
const ctrParent = tmp();
const ctrDir = path.join(ctrParent, 'plugins');
try {
const reg = [{ cli: 'claude', host: hostDir, ctr: ctrDir, files: ['installed_plugins.json'] }];
fs.writeFileSync(
path.join(hostDir, 'installed_plugins.json'),
JSON.stringify({ 'p@m': [{ installPath: 'C:\\Users\\g\\.claude\\plugins\\cache\\p\\1.0' }] }),
);
translate(reg);
const first = fs.readFileSync(path.join(ctrDir, 'installed_plugins.json'), 'utf8');
translate(reg);
const second = fs.readFileSync(path.join(ctrDir, 'installed_plugins.json'), 'utf8');
assert.equal(first, second);
} finally {
fs.rmSync(hostDir, { recursive: true, force: true });
fs.rmSync(ctrParent, { recursive: true, force: true });
}
});
test('translate: malformed host registry is skipped, dst not written', () => {
const hostDir = tmp();
const ctrParent = tmp();
const ctrDir = path.join(ctrParent, 'plugins');
try {
const reg = [{ cli: 'claude', host: hostDir, ctr: ctrDir, files: ['installed_plugins.json'] }];
fs.writeFileSync(path.join(hostDir, 'installed_plugins.json'), '{ broken');
translate(reg);
assert.equal(fs.existsSync(path.join(ctrDir, 'installed_plugins.json')), false);
} finally {
fs.rmSync(hostDir, { recursive: true, force: true });
fs.rmSync(ctrParent, { recursive: true, force: true });
}
});
test('translate: empty and missing host registries are skipped without error', () => {
const hostDir = tmp();
const ctrParent = tmp();
const ctrDir = path.join(ctrParent, 'plugins');
try {
const reg = [
{ cli: 'claude', host: hostDir, ctr: ctrDir, files: ['empty.json', 'missing.json'] },
];
fs.writeFileSync(path.join(hostDir, 'empty.json'), ''); // we never create missing.json
translate(reg);
assert.equal(fs.existsSync(path.join(ctrDir, 'empty.json')), false);
assert.equal(fs.existsSync(path.join(ctrDir, 'missing.json')), false);
} finally {
fs.rmSync(hostDir, { recursive: true, force: true });
fs.rmSync(ctrParent, { recursive: true, force: true });
}
});
// --- selectRegistries: the per-CLI filter post-create.sh drives translate with
test('selectRegistries: no filter -> all registries (original behavior)', () => {
const regs = [{ cli: 'claude' }, { cli: 'cursor' }];
assert.deepEqual(selectRegistries(regs, []), regs);
assert.deepEqual(selectRegistries(regs, undefined), regs);
});
test('selectRegistries: filter keeps only the named CLIs', () => {
const regs = [{ cli: 'claude' }, { cli: 'cursor' }];
assert.deepEqual(selectRegistries(regs, ['claude']), [{ cli: 'claude' }]);
assert.deepEqual(selectRegistries(regs, ['cursor']), [{ cli: 'cursor' }]);
assert.deepEqual(selectRegistries(regs, ['claude', 'cursor']), regs);
});
test('selectRegistries: an unknown CLI name selects nothing', () => {
const regs = [{ cli: 'claude' }, { cli: 'cursor' }];
assert.deepEqual(selectRegistries(regs, ['codex']), []);
});
test('selectRegistries: empty registry table stays empty under any filter', () => {
assert.deepEqual(selectRegistries([], ['claude']), []);
assert.deepEqual(selectRegistries([], []), []);
});
// --- seed-claude-config main(): end-to-end, through the real CLI entry point
const SEED_SCRIPT = path.join(__dirname, 'seed-claude-config.cjs');
test('seed main: strips machine fields, keeps account, sets onboarding, chmod 644', () => {
const dir = tmp();
try {
const src = path.join(dir, 'host.claude.json');
const dst = path.join(dir, 'out.claude.json');
fs.writeFileSync(
src,
JSON.stringify({
installMethod: 'native',
userID: 'abc',
oauthAccount: { emailAddress: 'x@y.z' },
}),
);
execFileSync(process.execPath, [SEED_SCRIPT, src, dst]);
const out = JSON.parse(fs.readFileSync(dst, 'utf8'));
assert.equal(out.installMethod, undefined);
assert.equal(out.userID, 'abc');
assert.equal(out.oauthAccount.emailAddress, 'x@y.z');
assert.equal(out.hasCompletedOnboarding, true);
if (process.platform !== 'win32') {
assert.equal(fs.statSync(dst).mode & 0o777, 0o644);
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('seed main: missing host file still writes a valid onboarding-bearing file', () => {
const dir = tmp();
try {
const dst = path.join(dir, 'out.claude.json');
execFileSync(process.execPath, [SEED_SCRIPT, path.join(dir, 'nope.json'), dst]);
assert.deepEqual(JSON.parse(fs.readFileSync(dst, 'utf8')), { hasCompletedOnboarding: true });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('seed main: chmodSync widens a pre-existing restrictive dst to 0o644', () => {
// This checks the file's permission bits, which only exist on POSIX systems.
//
// The catch: CI's default umask is 022, so a plain writeFileSync already
// creates files at mode 0o644. Asserting 0o644 right after a fresh write
// would therefore NOT prove the explicit chmodSync did anything.
//
// So we pre-create dst at the stricter mode 0o600. Opening a file in 'w'
// mode replaces its contents but KEEPS the mode of a file that already
// exists. That means the only way dst can end up at 0o644 is the chmodSync
// inside seed-claude-config.cjs. This pins the test to the chmod and not to
// the umask: delete the chmodSync line and this test fails, while the other
// seed test still passes.
if (process.platform === 'win32') return;
const dir = tmp();
try {
const src = path.join(dir, 'host.claude.json');
const dst = path.join(dir, 'out.claude.json');
fs.writeFileSync(src, JSON.stringify({ userID: 'u' }));
fs.writeFileSync(dst, '{}');
fs.chmodSync(dst, 0o600);
execFileSync(process.execPath, [SEED_SCRIPT, src, dst]);
assert.equal(fs.statSync(dst).mode & 0o777, 0o644);
assert.equal(JSON.parse(fs.readFileSync(dst, 'utf8')).userID, 'u');
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// --- ensurePaths: sets up the host paths the bind mounts point at -----------
test('ensurePaths: creates every DIR and FILE under a temp home, idempotently', () => {
const home = tmp();
try {
ensurePaths(home);
for (const d of DIRS) {
assert.equal(fs.statSync(path.join(home, d)).isDirectory(), true, `not a dir: ${d}`);
}
for (const f of FILES) {
assert.equal(fs.statSync(path.join(home, f)).isFile(), true, `not a file: ${f}`);
}
// Running it again must not throw and must not overwrite existing content.
fs.writeFileSync(path.join(home, '.claude.json'), '{"keep":true}');
ensurePaths(home);
assert.equal(fs.readFileSync(path.join(home, '.claude.json'), 'utf8'), '{"keep":true}');
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
test('ensurePaths: does NOT pre-create settings.json / config.toml (no gratuitous host mutation)', () => {
const home = tmp();
try {
ensurePaths(home);
assert.equal(fs.existsSync(path.join(home, '.claude', 'settings.json')), false);
assert.equal(fs.existsSync(path.join(home, '.codex', 'config.toml')), false);
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
test('ensurePaths: does NOT pre-create the shareable subdirs (now copied, not bound)', () => {
// The shareable dirs are seeded into the per-container volume from the
// read-only /host stage, so they are no longer bind-mount sources. Pre-creating
// empty ones would needlessly write into the host of someone who never used a
// CLI. This pins the DIRS trim: re-adding any of these would fail the test.
const home = tmp();
const mustNotExist = [
path.join('.claude', 'skills'),
path.join('.claude', 'agents'),
path.join('.claude', 'memory'),
path.join('.claude', 'commands'),
path.join('.claude', 'plugins'),
path.join('.codex', 'plugins'),
path.join('.codex', 'prompts'),
path.join('.codex', 'memories'),
path.join('.codex', 'skills'),
path.join('.cursor', 'rules'),
path.join('.cursor', 'commands'),
path.join('.cursor', 'agents'),
path.join('.cursor', 'skills'),
path.join('.cursor', 'plugins'),
];
try {
ensurePaths(home);
for (const sub of mustNotExist) {
assert.equal(fs.existsSync(path.join(home, sub)), false, `should not pre-create: ${sub}`);
}
// The top-level stage roots that ARE still bind sources must exist.
for (const top of ['.claude', '.codex', '.cursor', '.claude-mem']) {
assert.equal(fs.statSync(path.join(home, top)).isDirectory(), true, `missing root: ${top}`);
}
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
});
@@ -0,0 +1,19 @@
description = "GitNexus production-readiness PR swarm review (Solo mode)"
prompt = """
You are the GitNexus PR review coordinator. Review this pull request: {{args}}
(a PR URL or number for https://github.com/abhigyanpatwari/GitNexus). If no target was
given, ask for one.
Read `pr-swarm-review/orchestration.md` in this repository and follow it exactly. It is the
canonical, CLI-neutral review contract (lanes, classifications, output structure, finding
format, hidden-Unicode checks, behavior rules).
Run in **Solo mode**: you are a single agent, so perform all seven lanes yourself in
dependency order, adopting each persona in `pr-swarm-review/personas/0N-*.md` in turn
(lanes 1-2 first, then 3-6, then lane 7). Keep every lane's findings in context. Lane 7
(synthesis critic) is a hard gate: do not emit the final review until its "Required
corrections before posting" section is empty — revise and re-run it otherwise.
Stay strictly read-only: investigate and report; never edit files, commit, or post to GitHub.
"""
+15
View File
@@ -1,2 +1,17 @@
* text=auto eol=lf
.husky/* text eol=lf
# Shell scripts: force LF unconditionally so devcontainer scripts
# (e.g. anything COPYed into a Linux container) execute correctly when
# checked out on Windows hosts with core.autocrlf=true.
*.sh text eol=lf
*.bash text eol=lf
# Native and binary assets shouldn't be treated as text under any
# auto-detection or eol normalization.
*.node binary
*.wasm binary
*.onnx binary
*.so binary
*.dll binary
*.dylib binary
+4
View File
@@ -0,0 +1,4 @@
# Code owners
* @Arvuno
* @magyargergo
@@ -0,0 +1,19 @@
---
description: 'GitNexus production-readiness PR swarm review (Solo mode)'
mode: 'agent'
---
You are the GitNexus PR review coordinator. Review the pull request the user names (a PR URL
or number for `https://github.com/abhigyanpatwari/GitNexus`). If none was given, ask for one.
Read `pr-swarm-review/orchestration.md` in this repository and follow it exactly — it is the
canonical, CLI-neutral review contract (lanes, classifications, output structure, finding
format, hidden-Unicode checks, behavior rules).
Run in **Solo mode**: you are a single agent, so perform all seven lanes yourself in
dependency order, adopting each persona in `pr-swarm-review/personas/0N-*.md` in turn
(lanes 1–2 first, then 3–6, then lane 7). Keep every lane's findings in context. Lane 7
(synthesis critic) is a hard gate: do not emit the final review until its "Required
corrections before posting" section is empty.
Stay strictly read-only: investigate and report; never edit files, commit, or post to GitHub.
@@ -332,6 +332,111 @@ def vendored_drift_summary(
}
# ── Assert mode (CI gate) ─────────────────────────────────────────────────
def assert_current() -> int:
"""Assert every grammar's ABI is loadable by the CURRENT runtime.
Unlike the readiness report (which probes the npm registry + upstream
main for the *target* runtime), this mode is hermetic and offline: it
reads only what's checked out / installed locally and asserts each
grammar's compiled ABI lies within the current runtime's
``RUNTIME_ABI_RANGES`` window. It is the static half of the #1922 ABI
gate; the runtime load-smoke (`parser-loader-abi.test.ts`) is the
dynamic half.
Coverage, reusing the existing helpers:
- npm-installed grammars: ABI from node_modules/<name>/<parser.c>.
- vendored grammars (dart/proto/swift): ABI via ``vendored_drift_summary``.
- Swift is prebuilt-only (no parser.c) → not introspectable here;
treated as "covered by the runtime load-smoke", not asserted.
- INTENTIONAL_PINS are honored: a pinned grammar is expected to sit at
an ABI the current runtime loads (that's *why* it's pinned), so it is
asserted like any other rather than skipped.
Returns 0 when every introspectable grammar is in range, 1 otherwise.
Prints a plain-text (non-Markdown) report so CI logs stay readable.
"""
current_runtime = read_current_runtime()
abi_range = RUNTIME_ABI_RANGES.get(current_runtime)
if abi_range is None:
print(
f"FAIL: RUNTIME_ABI_RANGES has no entry for current runtime "
f"{current_runtime!r}; add it before asserting.",
)
return 1
lo, hi = abi_range
pinned_versions = read_pinned_grammar_versions()
print(
f"Asserting all grammar ABIs load on tree-sitter@{current_runtime}.x "
f"(ABI {lo}–{hi})."
)
failures: list[str] = []
checked = 0
skipped: list[str] = []
for name, (upstream_repo, upstream_branch, parser_path) in sorted(GRAMMARS.items()):
pinned_spec = pinned_versions.get(name, "—")
pin_note = f" [intentional pin: {pinned_spec}]" if name in INTENTIONAL_PINS else ""
if is_vendored_pin(pinned_spec):
v = vendored_drift_summary(name, upstream_repo, upstream_branch, parser_path)
abi = v["vendored_abi"]
if abi is None:
# Prebuilt-only vendor (e.g. tree-sitter-swift): no parser.c to
# introspect. The runtime load-smoke covers it instead.
skipped.append(f"{name} (vendored, prebuilt — covered by load-smoke)")
continue
checked += 1
if lo <= abi <= hi:
print(f" OK {name}: vendored ABI {abi} in range{pin_note}")
else:
msg = (
f"{name}: vendored ABI {abi} outside current runtime range "
f"{lo}..{hi}{pin_note}"
)
print(f" FAIL {msg}")
failures.append(msg)
continue
installed_parser = GITNEXUS_DIR / "node_modules" / name / parser_path
if not installed_parser.is_file():
installed_parser = GITNEXUS_DIR / "node_modules" / name / "src" / "parser.c"
abi = extract_language_version(installed_parser)
if abi is None:
skipped.append(f"{name} (not installed / no parser.c — covered by load-smoke)")
continue
checked += 1
if lo <= abi <= hi:
print(f" OK {name}: installed ABI {abi} in range{pin_note}")
else:
msg = (
f"{name}: installed ABI {abi} outside current runtime range "
f"{lo}..{hi}{pin_note}"
)
print(f" FAIL {msg}")
failures.append(msg)
print("")
if skipped:
print("Not statically introspectable (asserted via runtime load-smoke):")
for s in skipped:
print(f" - {s}")
print("")
if failures:
print(f"RESULT: FAIL — {len(failures)} grammar(s) out of range, {checked} checked.")
for f in failures:
print(f" - {f}")
return 1
print(f"RESULT: OK — all {checked} introspectable grammar ABIs in range.")
return 0
# ── Main ────────────────────────────────────────────────────────────────
@@ -806,4 +911,9 @@ if __name__ == "__main__":
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
except Exception:
pass
# `--assert-current` is the offline CI gate (#1922): assert every grammar's
# ABI loads on the CURRENT runtime. Bare invocation keeps the original
# target-runtime readiness report behaviour.
if "--assert-current" in sys.argv[1:]:
sys.exit(assert_current())
sys.exit(main())
+127
View File
@@ -0,0 +1,127 @@
name: Devcontainer Smoke
# Smoke-tests .devcontainer/ whenever it changes. Two things happen here.
# First, unit tests run on the pure host->container config transforms: the
# plugin-registry path translation, and the strip of the machine field from
# $HOME/.claude.json. Second, the devcontainer image is built through the
# standard @devcontainers/cli path. That CLI reads build.args from
# devcontainer.json, so the version pin there stays the single source of truth.
on:
push:
branches: [main]
paths:
- '.devcontainer/**'
- '.github/workflows/ci-devcontainer.yml'
pull_request:
paths:
- '.devcontainer/**'
- '.github/workflows/ci-devcontainer.yml'
permissions:
contents: read
# Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
# Grouped per branch or tag. Cancel a PR run when a newer one replaces it.
# Never cancel a push-to-main run.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
config-transforms:
name: Config-transform unit tests
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# persist-credentials: false — this job only reads (tests and syntax
# checks) and never pushes. The setting keeps GITHUB_TOKEN out of
# .git/config, which zizmor flags as the "artipacked" issue.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
- name: Unit-test the host->container config transforms
run: node --test .devcontainer/translate-plugin-registries.test.cjs
- name: Syntax-check the lifecycle shell scripts
run: |
bash -n .devcontainer/install-deps.sh
bash -n .devcontainer/post-create.sh
build:
name: Build devcontainer image
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# persist-credentials: false — this is a read-only build smoke that
# never pushes. The setting keeps GITHUB_TOKEN out of .git/config,
# which zizmor flags as the "artipacked" issue.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
# Builds the image the same way a developer's "Reopen in Container" does.
# @devcontainers/cli reads devcontainer.json (jsonc format), resolves
# build.args (the CLAUDE_CODE_VERSION / CODEX_VERSION pins), and runs the
# Dockerfile. This smoke catches Dockerfile regressions and any drift from
# the canonical version pins. The lifecycle hooks (post-create.sh) do not
# run here. They need the host config mounts, and CI has none.
#
# ARCH COVERAGE: this runs on an x64 runner with no --platform or QEMU, so
# it builds only the amd64 Cursor branch (CURSOR_SHA256_X64). The arm64
# branch (CURSOR_SHA256_ARM64 plus the arm64 tarball URL) is pinned by a
# sha256 checked against the published artifact, but it is not BUILT here.
# Cursor's extract-and-symlink step does not depend on the architecture, so
# the only remaining gap is a stale arm64 URL or hash. If that becomes a
# concern, add a linux/arm64 matrix leg (docker/setup-qemu-action plus
# `--platform`).
#
# The @devcontainers/cli version is pinned on purpose. A bare
# `npx --yes @devcontainers/cli` would resolve @latest at run time. A
# breaking or malicious publish could then change CI behavior, or change
# how devcontainer.json is read, with no diff to show for it. Bump this pin
# deliberately, alongside the Dockerfile and devcontainer.json pins.
#
# @devcontainers/cli wraps the Dockerfile with `# syntax=docker/dockerfile:1`,
# which BuildKit resolves from Docker Hub. Hub blips surface as
# `DeadlineExceeded` / `i/o timeout` on the syntax frontend (see run
# 26797815133). Build retry (2 attempts, 45s backoff) matches
# `.github/actions/docker-build-push-retry` (docker/build-push-action#1422).
# Pre-pull of docker/dockerfile:1 is extra hardening; best-effort so the
# build retry still runs if Hub is flaky only during pull.
- name: Pre-pull BuildKit Dockerfile frontend (retry)
continue-on-error: true
run: |
set -euo pipefail
img="docker/dockerfile:1"
for attempt in 1 2 3; do
if docker pull "$img"; then
exit 0
fi
echo "::warning::docker pull ${img} attempt ${attempt} failed"
if [ "$attempt" -lt 3 ]; then
sleep $((attempt * 15))
fi
done
echo "::warning::failed to pre-pull ${img} after 3 attempts; continuing — build step may still succeed"
exit 1
- name: Build devcontainer via @devcontainers/cli
run: |
set -euo pipefail
for attempt in 1 2; do
if npx --yes @devcontainers/cli@0.87.0 build --workspace-folder .; then
if [ "$attempt" -eq 2 ]; then
echo "::notice::devcontainer build retry succeeded (attempt 2); investigate if this recurs across runs."
fi
exit 0
fi
if [ "$attempt" -eq 2 ]; then
echo "::error::devcontainer build failed after 2 attempts"
exit 1
fi
echo "::warning::devcontainer build attempt ${attempt} failed; retrying in 45s…"
sleep 45
done
-87
View File
@@ -1,87 +0,0 @@
name: Scope Resolution Parity
# Reusable workflow — called from ci.yml. Does NOT declare concurrency;
# it inherits the caller's concurrency group per the convention documented
# in CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
#
# ── Purpose (RFC #909 Ring 3, §6.4 "Observability gates") ──────────────
# For every language in `MIGRATED_LANGUAGES` (exported from
# `gitnexus/src/core/ingestion/registry-primary-flag.ts`), run the
# resolver integration test at `test/integration/resolvers/<slug>.test.ts`
# TWICE on every PR:
#
# 1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path (guarantees we haven't
# broken the old path while migrating). Known legacy gaps may be skipped
# through the resolver test helper's expected-failure list.
# 2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path (guarantees the
# new path carries the same behavior — the parity gate).
#
# BOTH must pass. The source of truth is the TypeScript constant — adding
# a language to that `Set` is the ONLY contributor action; CI auto-
# discovers it, runs parity, and the language's default production path
# flips to registry-primary in the same change.
#
# When the set is empty (e.g. mid-Ring-3 for every language), the parity
# matrix is skipped and the workflow reports success — no-op until a
# language is explicitly claimed migrated.
#
# ── Consolidation (chore/vitest-speed-strategy) ────────────────────────
# Previously each language was a separate GitHub Actions matrix job,
# meaning N languages × 1 checkout+install+build per shard. The build
# cost dwarfed the test cost (~5 min setup for ~15 sec test execution).
#
# Now a single job runs `scripts/run-parity.ts` which loops through all
# migrated languages sequentially (2 vitest invocations per language:
# legacy + registry-primary). All failures are collected and reported
# at the end (equivalent to the old fail-fast: false behavior).
#
# Adding a new language to MIGRATED_LANGUAGES still requires no workflow
# edit — the script auto-discovers the set at runtime.
on:
workflow_call:
permissions:
contents: read
jobs:
discover:
name: Discover migrated languages
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
has-any: ${{ steps.read.outputs.has-any }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-gitnexus
- name: Extract MIGRATED_LANGUAGES from registry-primary-flag.ts
id: read
shell: bash
working-directory: gitnexus
run: |
set -euo pipefail
LANGS=$(npx tsx scripts/ci-list-migrated-languages.ts)
COUNT=$(printf '%s' "$LANGS" | jq 'length')
HAS_ANY="false"
if [[ "$COUNT" -gt 0 ]]; then HAS_ANY="true"; fi
echo "has-any=$HAS_ANY" >> "$GITHUB_OUTPUT"
echo "Discovered $COUNT migrated language(s): $LANGS"
echo "Parity will run: $HAS_ANY"
parity:
name: scope-resolution parity
needs: discover
if: needs.discover.outputs.has-any == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
- name: Run parity for all migrated languages
shell: bash
working-directory: gitnexus
run: npx tsx scripts/run-parity.ts
+95
View File
@@ -12,7 +12,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
# persist-credentials: false — this job runs tests and uploads a
# test-reports artifact (if: always()). The default-persisted token in
# .git/config must not be capturable through that upload (zizmor
# credential-persistence / artipacked audit). The job never pushes.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
@@ -72,7 +78,11 @@ jobs:
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
# persist-credentials: false — runs tests only, never pushes (zizmor
# credential-persistence / artipacked audit).
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
@@ -80,6 +90,34 @@ jobs:
run: npx tsx scripts/run-cross-platform.ts
working-directory: gitnexus
# Tree-sitter ABI gate (#1922). Two halves, both blocking:
# 1. Static, offline: assert every grammar's compiled ABI loads on the
# pinned runtime (check-tree-sitter-upgrade-readiness.py --assert-current).
# 2. Dynamic: run the parser-loader ABI load-smoke on the OS matrix so an
# ABI-incompatible prebuilt (esp. the binary-only Swift vendor, which the
# static check can't introspect) fails on the platform it ships to.
abi-assert:
name: tree-sitter ABI (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
- name: Assert installed + vendored grammar ABIs (static)
shell: bash
run: python3 .github/scripts/check-tree-sitter-upgrade-readiness.py --assert-current
- name: Run parser-loader ABI load-smoke (dynamic)
run: npx vitest run test/unit/parser-loader-abi.test.ts
working-directory: gitnexus
# End-to-end smoke test for the #1728 packaging fix: pack the published
# tarball, install it globally into a temp prefix, and assert no junction
# creation (the EPERM root cause) plus working CLI plus vendor cleanliness
@@ -181,3 +219,60 @@ jobs:
else
"$PREFIX/bin/gitnexus" --version
fi
# ── Dedicated benchmark gate ─────────────────────────────────────
# The cross-language `*-pipeline-benchmark.test.ts` suites are gated behind
# GITNEXUS_BENCH (they generate synthetic codebases at scale), so the main
# coverage job above SKIPS them — their O(n^2) scaling guards never ran in CI.
# Run them here with GITNEXUS_BENCH=1, alongside the Python scope-capture and
# import-resolution fingerprint + scaling guards (PR #1918 P2a).
#
# `--no-file-parallelism` is REQUIRED: these suites measure wall-clock and peak
# heap, so parallel forks both skew the timings and OOM the worker pool — they
# must run one file at a time.
#
# go-pipeline-benchmark.test.ts is deliberately NOT included: its
# worker-pool (#1848) suite spins a real worker pool that exits unexpectedly
# under vitest's fork pool (reproduced in validation), which would make this
# gate flaky. Go is already guarded by its non-gated O(n^2) tripwire (runs in
# the main coverage job) plus its golden capture-parity test.
benchmarks:
name: benchmarks (GITNEXUS_BENCH)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
# persist-credentials: false — this job only runs npm + vitest benchmarks
# and never pushes; the default-persisted token in .git/config would be at
# risk of leaking through an artifact upload (zizmor credential-persistence
# / artipacked audit). Mirrors the packaged-install-smoke job below.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./.github/actions/setup-gitnexus
with:
build: 'true'
- name: Python scope-capture + import-resolution fingerprint / scaling guards
run: |
node --import tsx bench/python-scope/measure.mjs --check
node --import tsx bench/python-scope/import-target-fingerprint.mjs --check
working-directory: gitnexus
- name: Cross-language scope-capture fingerprint + scaling guards
# Build-free: asserts emit<Lang>ScopeCaptures output is unchanged
# (fingerprint) and stays linear (scaling < 1.5) for go/csharp/rust/php/
# ruby/cobol. Catches an O(n^2) re-regression without the worker pool.
run: node --import tsx bench/scope-capture/measure.mjs --check
working-directory: gitnexus
- name: Cross-language pipeline benchmarks (GITNEXUS_BENCH, serial)
env:
GITNEXUS_BENCH: '1'
run: >-
npx vitest run --no-file-parallelism
test/integration/cobol-pipeline-benchmark.test.ts
test/integration/csharp-pipeline-benchmark.test.ts
test/integration/rust-pipeline-benchmark.test.ts
test/integration/php-pipeline-benchmark.test.ts
test/integration/ruby-pipeline-benchmark.test.ts
working-directory: gitnexus
+15 -25
View File
@@ -27,9 +27,8 @@ concurrency:
# Each concern lives in its own workflow file for maintainability:
# ci-quality.yml — typecheck (tsc --noEmit)
# ci-tests.yml — unit + integration tests with coverage + cross-platform
# (includes the scope-resolution resolver tests)
# ci-e2e.yml — E2E tests (only when gitnexus-web/ changes)
# ci-scope-parity.yml — RFC #909 Ring 3 parity gate: legacy DAG + registry-primary
# both pass, per migrated language in the JSON registry
#
# Shared setup is DRY via .github/actions/setup-gitnexus composite action.
@@ -49,11 +48,6 @@ jobs:
permissions:
contents: read
scope-parity:
uses: ./.github/workflows/ci-scope-parity.yml
permissions:
contents: read
# ── Save PR metadata for the reporting workflow ─────────────────
# The ci-report.yml workflow (triggered by workflow_run) needs the
# PR number and job results to post a comment. We save them as an
@@ -62,7 +56,7 @@ jobs:
save-pr-meta:
name: Save PR Metadata
if: always() && github.event_name == 'pull_request'
needs: [quality, tests, e2e, scope-parity]
needs: [quality, tests, e2e]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
@@ -73,14 +67,12 @@ jobs:
QUALITY: ${{ needs.quality.result }}
TESTS: ${{ needs.tests.result }}
E2E: ${{ needs.e2e.result }}
SCOPE_PARITY: ${{ needs.scope-parity.result }}
run: |
mkdir -p pr-meta
echo "$PR_NUMBER" > pr-meta/pr_number
echo "$QUALITY" > pr-meta/quality_result
echo "$TESTS" > pr-meta/tests_result
echo "$E2E" > pr-meta/e2e_result
echo "$SCOPE_PARITY" > pr-meta/scope_parity_result
# TODO(post-merge): remove backward-compat copies once ci-report.yml
# on main reads underscore names.
# Backward-compat: ci-report.yml on main still reads hyphenated
@@ -103,7 +95,7 @@ jobs:
# Single required check for branch protection.
ci-status:
name: CI Gate
needs: [quality, tests, e2e, scope-parity]
needs: [quality, tests, e2e]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
@@ -112,31 +104,29 @@ jobs:
shell: bash
env:
QUALITY: ${{ needs.quality.result }}
# The tree-sitter ABI gate (#1922) runs as the `abi-assert` job
# inside the `tests` reusable workflow. A failed job fails the
# reusable workflow, so `needs.tests.result` below blocks the merge
# on an ABI mismatch. (`jobs.<id>.result` cannot be exposed as a
# workflow_call output, so the gate is enforced transitively here.)
# The scope-resolution resolver tests also run inside the `tests`
# workflow (RING4-1 #942 removed the separate scope-parity gate),
# so a resolver regression makes TESTS != success and blocks here.
TESTS: ${{ needs.tests.result }}
E2E: ${{ needs.e2e.result }}
SCOPE_PARITY: ${{ needs.scope-parity.result }}
run: |
echo "Quality: $QUALITY"
echo "Tests: $TESTS"
echo "E2E: $E2E"
echo "Scope parity: $SCOPE_PARITY"
# A failed `abi-assert` job (#1922) inside the tests reusable
# workflow makes TESTS != success, so this clause also blocks the
# merge on a tree-sitter ABI mismatch.
if [[ "$QUALITY" != "success" ]] ||
[[ "$TESTS" != "success" ]]; then
echo "::error::Quality or test jobs failed"
echo "::error::Quality or test jobs failed (includes the tree-sitter ABI gate, #1922)"
exit 1
fi
if [[ "$E2E" != "success" && "$E2E" != "skipped" ]]; then
echo "::error::E2E job failed"
exit 1
fi
# scope-parity is a reusable workflow. With an empty migrated-
# languages list, its parity matrix is skipped and the outer
# workflow still reports `success`. If any entry's legacy-DAG or
# registry-primary run fails, the workflow reports `failure`.
# Accept only `success`; `skipped` would mean the entire
# discover job was skipped too (upstream failure), which should
# still block.
if [[ "$SCOPE_PARITY" != "success" ]]; then
echo "::error::Scope-resolution parity gate failed (RFC #909 Ring 3)"
exit 1
fi
+2 -2
View File
@@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
languages: ${{ matrix.language }}
queries: security-and-quality
@@ -69,6 +69,6 @@ jobs:
- '**/test/fixtures/**'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
category: '/language:${{ matrix.language }}'
+4 -4
View File
@@ -141,14 +141,14 @@ jobs:
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Log in to GitHub Container Registry
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -163,7 +163,7 @@ jobs:
# `akonlabs/gitnexus` and `akonlabs/gitnexus-web` repos.
- name: Log in to Docker Hub
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -183,7 +183,7 @@ jobs:
# `github.event_name` would still be "push", not "workflow_call".
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
# Dual-registry publish. metadata-action expands the same tag set
# against every image ref listed here, and build-push-action pushes
+12
View File
@@ -38,6 +38,18 @@ jobs:
# steps (and Gitleaks itself) don't need it for repo operations.
persist-credentials: false
# gitleaks-action builds `base^..head` for pull_request events; both SHAs
# must exist locally (fork PRs and shallow checkouts otherwise fail with
# "unknown revision" — see gitleaks/gitleaks-action#199).
- name: Fetch PR refs for gitleaks range
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git fetch --no-tags origin "$BASE_SHA"
git fetch --no-tags origin "$HEAD_SHA"
# No GITLEAKS_LICENSE secret is required for OSS / public-repo usage.
# If this repo becomes private, the action will require a license key.
- name: Gitleaks
+1 -1
View File
@@ -257,7 +257,7 @@ jobs:
# ── Phase 3: reusable CI gate ──────────────────────────────────────────────
# Runs for both rc (when guard says go) and stable. No `secrets:` passed —
# ci.yml and its entire reusable-workflow chain (ci-quality, ci-tests,
# ci-e2e, ci-scope-parity, ci-report) reference zero `secrets.*` values;
# ci-e2e, ci-report) reference zero `secrets.*` values;
# passing any would be unused surface. GITHUB_TOKEN is implicit.
ci:
needs: [route, rc-guard]
+1 -1
View File
@@ -53,6 +53,6 @@ jobs:
retention-days: 5
- name: Upload to Security tab
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: results.sarif
+3 -3
View File
@@ -50,10 +50,10 @@ jobs:
persist-credentials: false
- name: Setup Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Build image (load locally for scan)
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: ${{ matrix.image.dockerfile }}
@@ -76,7 +76,7 @@ jobs:
exit-code: '0'
- name: Upload to Security tab
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: trivy-${{ matrix.image.name }}.sarif
category: trivy-${{ matrix.image.name }}
+1 -1
View File
@@ -76,7 +76,7 @@ jobs:
continue-on-error: true
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: zizmor.sarif
category: zizmor
+4 -2
View File
@@ -91,11 +91,13 @@ gitnexus/vendor/**/node_modules/
.claude-flow/
.claude/agents/
.claude/agents/*
!.claude/agents/gitnexus-*.md
.claude/commands/
.claude/helpers
.claude/skills/
.claude/skills/*
!.claude/skills/gitnexus/
!.claude/skills/gitnexus-pr-swarm-review/
.history/
+12
View File
@@ -0,0 +1,12 @@
title = "GitNexus"
[extend]
useDefault = true
# Fake embedding API keys in unit tests (current probe + historical placeholder).
[allowlist]
description = "fake embedding API keys in http-embedder unit tests"
regexes = [
'''secret-key-12345''',
'''test-api-key-redaction-check''',
]
+21 -10
View File
@@ -39,11 +39,22 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING.
## Reference docs
- **[ARCHITECTURE.md](ARCHITECTURE.md)**, **[CONTRIBUTING.md](CONTRIBUTING.md)**, **[GUARDRAILS.md](GUARDRAILS.md)**
- **Call-resolution DAG (legacy path):** See ARCHITECTURE.md § Call-Resolution DAG. Typed 6-stage DAG inside the `parse` phase; language-specific behavior behind `inferImplicitReceiver` / `selectDispatch` hooks on `LanguageProvider`. Shared code in `gitnexus/src/core/ingestion/` must not name languages. Types: `gitnexus/src/core/ingestion/call-types.ts`.
- **Scope-resolution pipeline (RFC #909 Ring 3):** See ARCHITECTURE.md § Scope-Resolution Pipeline. Replaces the legacy DAG for languages in `MIGRATED_LANGUAGES` (see `registry-primary-flag.ts`). A language plugs in by implementing `ScopeResolver` (`scope-resolution/contract/scope-resolver.ts`) and registering it in `SCOPE_RESOLVERS`. CI parity gate runs BOTH paths per migrated language on every PR.
- **Call & inheritance resolution (RFC #909 Ring 3):** See ARCHITECTURE.md § Scope-Resolution Pipeline. All languages resolve calls and inheritance through the scope-resolution pipeline (`Registry.lookup`, `preEmitInheritanceEdges`, `emitHeritageEdges`, `buildMro` → `MethodDispatchIndex`). **Shared code in `gitnexus/src/core/ingestion/` must not name languages** — plug language behavior in via `LanguageProvider` / `ScopeResolver` hooks. A language plugs in by implementing `ScopeResolver` (`scope-resolution/contract/scope-resolver.ts`) and registering it in `SCOPE_RESOLVERS`. (The legacy call-resolution DAG + `@heritage` capture path were removed in RING4-1 #942.)
- **Cursor:** `.cursor/index.mdc` (always-on); `.cursor/rules/*.mdc` (glob-scoped). Legacy `.cursorrules` deprecated.
- **GitNexus:** skills in `.claude/skills/gitnexus/`; MCP rules in `gitnexus:start` block below.
## PR Swarm Review (cross-CLI)
To run a production-readiness review of a GitNexus pull request from **any** AI CLI, follow
the canonical, CLI-neutral spec **[`pr-swarm-review/orchestration.md`](pr-swarm-review/orchestration.md)**
(seven read-only review personas under `pr-swarm-review/personas/`). It defines two
execution modes with the same output contract: **Swarm mode** (parallel subagents, e.g.
Claude Code) and **Solo mode** (one agent runs all lanes sequentially — Codex, Gemini,
Cursor, Copilot, or any agent reading this file). Per-CLI entrypoints are thin wrappers
listed in [`pr-swarm-review/README.md`](pr-swarm-review/README.md); edit review logic only
in the canonical files, never in the wrappers. The review is read-only — it never edits,
commits, or posts.
## Changelog
| Date | Version | Change |
@@ -65,22 +76,22 @@ Commands and gotchas live under **Repo reference** below and in **[CONTRIBUTING.
This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 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.
> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
## 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 run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `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 exploring unfamiliar code, use `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 `context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER edit a function, class, or method without first running `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.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
## Resources
+31 -114
View File
@@ -15,7 +15,7 @@ Monorepo: **CLI/MCP** (`gitnexus/`) + **browser UI** (`gitnexus-web/`).
## End-to-end flow: index → graph → tools
1. **Ingestion** — `analyze.ts` → `runFullAnalysis` (`run-analyze.ts`) → `runPipelineFromRepo` (`pipeline.ts`). DAG of 12 phases builds a `KnowledgeGraph` in memory, then loads into LadybugDB under `.gitnexus/`. Repo registered in `~/.gitnexus/registry.json` for MCP discovery.
1. **Ingestion** — `analyze.ts` → `runFullAnalysis` (`run-analyze.ts`) → `runPipelineFromRepo` (`pipeline.ts`). DAG of 14 phases builds a `KnowledgeGraph` in memory, then loads into LadybugDB under `.gitnexus/`. Repo registered in `~/.gitnexus/registry.json` for MCP discovery.
2. **Persistence** — `repo-manager.ts` (paths, registry, KuzuDB cleanup). `lbug-adapter.ts` (graph load, queries, embedding batches).
@@ -65,7 +65,7 @@ Monorepo: **CLI/MCP** (`gitnexus/`) + **browser UI** (`gitnexus-web/`).
| Wiki generation | `src/core/wiki/` |
| Language support | `src/core/ingestion/languages/` + `tree-sitter-queries.ts` + `gitnexus-shared/src/languages.ts` |
| Import resolution | `src/core/ingestion/import-processor.ts` + `import-resolvers/configs/` + `model/resolution-context.ts` |
| Call resolution/MRO | `src/core/ingestion/call-processor.ts` + `model/resolve.ts` |
| Call resolution/inheritance/MRO | `src/core/ingestion/scope-resolution/` (pipeline, passes, graph-bridge) |
| Type extraction | `src/core/ingestion/type-extractors/` |
| Worker pool | `src/core/ingestion/workers/` |
| Web UI | `gitnexus-web/src/` |
@@ -77,11 +77,11 @@ Monorepo: **CLI/MCP** (`gitnexus/`) + **browser UI** (`gitnexus-web/`).
## Pipeline Phase DAG
12 phases defined in `gitnexus/src/core/ingestion/pipeline-phases/`, each with explicit `deps` and typed output.
14 phases defined in `gitnexus/src/core/ingestion/pipeline-phases/`, each with explicit `deps` and typed output.
```
scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
→ crossFile → mro → communities → processes
→ crossFile → scopeResolution → pruneLocalSymbols → mro → communities → processes
```
| Phase | File | Deps | Output |
@@ -95,11 +95,13 @@ scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
| `tools` | `tools.ts` | `parse` | Tool nodes + HANDLES_TOOL edges |
| `orm` | `orm.ts` | `parse` | QUERIES edges (Prisma, Supabase) |
| `crossFile` | `cross-file.ts` + `cross-file-impl.ts` | `parse`, `routes`, `tools`, `orm` | Cross-file type propagation in topological import order |
| `mro` | `mro.ts` | `crossFile`, `structure` | METHOD_OVERRIDES + METHOD_IMPLEMENTS edges |
| `communities` | `communities.ts` | `mro`, `structure` | Community nodes + MEMBER_OF edges (Leiden algorithm) |
| `processes` | `processes.ts` | `communities`, `routes`, `tools`, `structure` | Process nodes + STEP_IN_PROCESS edges |
| `scopeResolution` | `scope-resolution/pipeline/phase.ts` | `parse`, `crossFile`, `structure` | Binding/reference + inheritance edges; disposes BindingAccumulator |
| `pruneLocalSymbols` | `prune-local-symbols.ts` | `scopeResolution` | Drops inert block-local `Const`/`Variable`/`Static` nodes (only a `File→DEFINES` edge) post-resolution |
| `mro` | `mro.ts` | `crossFile`, `scopeResolution`, `pruneLocalSymbols`, `structure` | METHOD_OVERRIDES + METHOD_IMPLEMENTS edges |
| `communities` | `communities.ts` | `mro`, `pruneLocalSymbols`, `structure` | Community nodes + MEMBER_OF edges (Leiden algorithm) |
| `processes` | `processes.ts` | `communities`, `routes`, `tools`, `pruneLocalSymbols`, `structure` | Process nodes + STEP_IN_PROCESS edges |
**Non-phase files in the same directory:** `parse-impl.ts`, `cross-file-impl.ts` (implementation), `wildcard-synthesis.ts` (whole-module import expansion), `orm-extraction.ts` (sequential ORM fallback), `types.ts`, `runner.ts`, `index.ts`.
**Non-phase files in the same directory:** `parse-impl.ts`, `cross-file-impl.ts` (implementation), `wildcard-synthesis.ts` (whole-module import expansion), `types.ts`, `runner.ts`, `index.ts`.
### DAG runner
@@ -119,7 +121,8 @@ scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
- **Single graph accumulator** — all phases mutate the same `KnowledgeGraph` in `ctx`; the graph is the primary output.
- **Typed phase access** — `getPhaseOutput<T>(deps, 'name')` for type-safe upstream results.
- **Binding accumulator lifecycle** — created in `parse`, disposed by `crossFile` (in `finally`). No other phase should take ownership.
- **Skippable phases** — `skipGraphPhases` omits MRO/communities/processes (faster tests). `skipWorkers` forces sequential parsing.
- **Skippable phases** — `skipGraphPhases` omits MRO/communities/processes (faster tests); `pruneLocalSymbols` still runs (it is graph cleanup, not analysis). `skipWorkers` is no longer a sequential escape hatch — it (like `--workers 0` / `GITNEXUS_WORKER_POOL_SIZE=0`) is rejected with an actionable error, since the worker pool is the sole parse path (§ Chunked parse-and-resolve).
- **Local-symbol pruning** — `pruneLocalSymbols` removes inert block-local value symbols after scope resolution has consumed them. Opt out per-call with `PipelineOptions.keepLocalValueSymbols` or globally with the `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` env var.
### How to add a new phase
@@ -147,105 +150,18 @@ export const myPhase: PipelinePhase<MyPhaseOutput> = {
---
## Call-Resolution DAG
## Semantic model
Typed 6-stage pipeline in `call-processor.ts` (inside the `parse` phase) that resolves method/function calls and emits CALLS edges. Language behavior plugs in at two `LanguageProvider` hook points (stages 3–4); shared code names no languages. Scope: call resolution only — import resolution, type extraction, heritage, and symbol-table population live in other phases.
`SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). The scope-resolution pipeline reads from here: `findOwnedMember`, `pickOverload`, and `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`.
### Stages
```
extract-call ──▶ classify-form ──▶ infer-receiver ──▶ select-dispatch ──▶ resolve-target ──▶ emit-edge
(1) (2) (3) [hook] (4) [hook] (5) (6)
```
| Stage | Produces | Location |
|-------|----------|----------|
| **extract-call** | `ExtractedCallSite` (name, form, receiver, argCount) | `call-extractors/` (per-language); runs in worker |
| **classify-form** | callForm (`free`/`member`/`constructor`) + arity | `call-analysis.ts` → `inferCallForm`; shared, runs in worker |
| **infer-receiver** | `ReceiverEnriched` (receiver type finalized) | `call-processor.ts`; shared default chain, then `inferImplicitReceiver` hook |
| **select-dispatch** | `DispatchDecision` (primary, fallback, ancestryView) | `selectDispatch` hook, falls back to shared default |
| **resolve-target** | `TieredCandidates` | `model/resolve.ts` → `lookupMethodByOwnerWithMRO` (MRO walk) |
| **emit-edge** | CALLS edge in graph | `call-processor.ts`; writes edge with confidence tier |
### Provider hooks
Both hooks are optional on `LanguageProvider`. Ruby is the only current implementer.
**`inferImplicitReceiver`** — called after shared infer-receiver defaults. Returns `ImplicitReceiverOverride | null`.
| | |
|---|---|
| Inputs | `calledName`, `callForm`, `receiverName`, `receiverTypeName`, `callNode` (AST), `filePath` |
| Non-null fields | `callForm`, `receiverName`, `receiverTypeName` (required); `receiverSource: 'implicit-self'` (fixed); `hint?` (opaque, passed to `selectDispatch`) |
| Null | Keep existing `ReceiverEnriched` state |
**`selectDispatch`** — called after infer-receiver (including hook). Returns `DispatchDecision | null`; null uses shared default (constructor → `primary:'constructor'`; typed receiver → `primary:'owner-scoped'`; else → `primary:'free'`).
| | |
|---|---|
| Inputs | `calledName`, `callForm`, `receiverName`, `receiverTypeName`, `receiverSource`, `hint` |
| Non-null fields | `primary: 'owner-scoped' \| 'free' \| 'constructor'`; `fallback?: 'free-arity-narrowed'`; `ancestryView?: 'instance' \| 'singleton'`; `hint?` |
**`DispatchDecision` field semantics:**
- `primary: 'owner-scoped'` — MRO walk from receiver's type; used when receiver type is known.
- `fallback: 'free-arity-narrowed'` — after owner-scoped miss, search free-call candidates by arity only (Ruby uses this for implicit-self calls that miss their owner's MRO).
- `ancestryView: 'singleton'` — walk singleton/class ancestry instead of instance ancestry (Ruby `def self.foo` bodies, so `extend`-ed methods are found).
### Adding language behavior
1. **Implicit receivers** — implement `inferImplicitReceiver`: return null if call already has a receiver; otherwise use `findEnclosingClassInfo` (`ast-helpers.ts`) to find the enclosing context, return `ImplicitReceiverOverride` with `receiverSource: 'implicit-self'`, and optionally set `hint` for `selectDispatch`.
2. **Custom dispatch** — implement `selectDispatch`: inspect `receiverSource` and `hint`, return `DispatchDecision` with `primary`, optional `fallback`, optional `ancestryView`; return null to keep shared defaults.
3. **MRO strategy** — confirm `mroStrategy` is `'first-wins'`, `'c3'`, `'ruby-mixin'`, or `'none'`; consumed by `lookupMethodByOwnerWithMRO`.
**Ruby example** (`languages/ruby.ts` + `utils/ruby-self-call.ts`): `inferImplicitReceiver` rewrites bare-identifier calls to `self.method` and sets `hint` to `'instance'`/`'singleton'`; `selectDispatch` uses hint for `ancestryView` and adds `fallback: 'free-arity-narrowed'` for implicit-self calls.
### Code references
| Module | Purpose |
|--------|---------|
| `core/ingestion/call-types.ts` | DAG types: `ReceiverEnriched`, `DispatchDecision`, `ImplicitReceiverOverride` |
| `core/ingestion/language-provider.ts` | Hook signatures: `inferImplicitReceiver`, `selectDispatch` |
| `core/ingestion/call-processor.ts` | `processCalls`: stages 3–6 |
| `core/ingestion/model/resolve.ts` | `lookupMethodByOwnerWithMRO`: stage 5 MRO walk |
| `core/ingestion/languages/ruby.ts` | Both hooks + `mroStrategy: 'ruby-mixin'` |
| `core/ingestion/utils/ruby-self-call.ts` | Bare-call rewrite for `inferImplicitReceiver` |
### Coexistence with the scope-resolution pipeline
The Call-Resolution DAG is the **legacy path**. RFC #909 Ring 3 introduces a parallel **scope-resolution pipeline** (next section) that replaces stages 1–6 with a scope-indexed registry lookup. Both paths ship side-by-side and are gated per-language via `MIGRATED_LANGUAGES` + the `REGISTRY_PRIMARY_<LANG>` env var.
- **Unmigrated language** → Call-Resolution DAG runs; scope-resolution phase is a no-op.
- **Migrated language** (currently: Python, C#) → scope-resolution owns CALLS/ACCESSES/USES emission; the legacy DAG gates off for that language via `isRegistryPrimary(lang)` checks in `call-processor.ts` and `import-processor.ts`.
- `import-processor` still populates `importMap` for migrated languages — heritage's `ctx.resolve` reads it to disambiguate parent classes. Only edge emission is gated.
- CI runs BOTH paths for every migrated language on every PR (`.github/workflows/ci-scope-parity.yml`); both must pass.
#### Same-graph guarantee
Edges emitted by the scope-resolution pipeline and edges emitted by the legacy DAG are indistinguishable to downstream consumers (MCP tools, HTTP API, embeddings, group bridge):
- **Node identity** — both paths use `generateId(...)` from `lib/utils.ts`, the same qualified-name keyspace, and the same node labels (`File`, `Folder`, `Class`, `Method`, `Function`, …). Overload disambiguation suffixes `parameterTypes` into the id consistently — see `scope-resolution/graph-bridge/ids.ts` and the legacy emitter in `call-processor.ts`.
- **Edge vocabulary** — both paths emit the same reasons: `'import-resolved' | 'global' | 'local-call' | 'same-file' | 'interface-dispatch' | 'read' | 'write'`. Migrating a language must not change which reasons consumers see for previously-resolved edges.
- **Confidence tier** — both paths attach a numeric `confidence` to each edge using the same scale.
The CI parity workflow (`.github/workflows/ci-scope-parity.yml`) runs both paths against every migrated language's fixture corpus and fails on any divergence.
#### Semantic-model source of truth
Two independent invariants.
**ParsedFile = the AST-level truth.** `ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact both resolution paths consume. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
**SemanticModel = the symbol-level truth.** `SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). Both paths read from here:
- Legacy Call-Resolution DAG → `call-processor` Tier 1/2/3 via `model.symbols.lookupExactAll`, `model.methods.lookupMethodByName`, `model.types.lookupClassByName`, `lookupMethodByOwnerWithMRO`.
- Scope-resolution pipeline → `findOwnedMember`, `pickOverload`, `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`.
`ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact the scope-resolution pipeline consumes. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
The scope-resolution pipeline additionally carries `WorkspaceResolutionIndex` for `Scope`-valued lookups (`classScopeByDefId`, `moduleScopeByFile`) that `SemanticModel` structurally cannot hold. No symbol-indexed duplicates exist outside `SemanticModel`.
**Write / read phase contract.** The model is mutable during three ordered phases and read-only afterward:
```
Phase 1: legacy parse ──► symbolTable.add fans into types/methods/fields
Phase 1: parse ──► symbolTable.add fans into types/methods/fields
Phase 2: scope-resolution ──► reconcileOwnership() registers corrected ownerIds
Phase 3: finalize ──► model.attachScopeIndexes(bundle) — one-shot freeze
─────────────────────────── phase boundary ───────────────────────────
@@ -255,7 +171,7 @@ The scope-resolution pipeline additionally carries `WorkspaceResolutionIndex` fo
`runScopeResolution` narrows `MutableSemanticModel` → `SemanticModel` at the phase boundary so downstream passes physically cannot mutate the model even accidentally.
**Transitional: reconciliation pass.** `reconcileOwnership` (`scope-resolution/pipeline/reconcile-ownership.ts`) is a shim for languages whose legacy extractor doesn't resolve `enclosingClassId` at parse time (Python class-body methods are the canonical case). It walks `parsed.localDefs[i].ownerId` after `populateOwners` and registers any missed methods/fields into the model. Idempotent — safe to re-run, safe alongside languages whose legacy extractor already carries `ownerId` (C#).
**Reconciliation pass.** `reconcileOwnership` (`scope-resolution/pipeline/reconcile-ownership.ts`) is a shim for languages whose parse-time extractor doesn't resolve `enclosingClassId` at parse time (Python class-body methods are the canonical case). It walks `parsed.localDefs[i].ownerId` after `populateOwners` and registers any missed methods/fields into the model. Idempotent — safe to re-run, safe alongside languages whose extractor already carries `ownerId` (C#).
The architectural end state is for every language's parse-time extractor to emit the correct `ownerId` directly, making reconciliation a no-op (tracked as a follow-up refactor). The dev-mode validator `validateOwnershipParity` surfaces any drift via `onWarn` under `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`.
@@ -265,7 +181,7 @@ References: `semantic-model.ts` file-head (full write/read contract); `contract/
## Scope-Resolution Pipeline (RFC #909 Ring 3)
Language-agnostic registry-primary resolver. Replaces the Call-Resolution DAG for migrated languages. Adding a language is one interface implementation (`ScopeResolver`) plus two registrations — no changes to shared code, no new pipeline phase.
Language-agnostic scope-resolution resolver. This is the resolution path for every language — it owns CALLS/ACCESSES/USES emission and inheritance edges. Adding a language is one interface implementation (`ScopeResolver`) plus one registration in the `SCOPE_RESOLVERS` map — no changes to shared code, no new pipeline phase. (RING4-1 #942 removed the legacy call-resolution DAG and the per-language `MIGRATED_LANGUAGES` flag, so `SCOPE_RESOLVERS` registration is all that's needed.)
### Pipeline stages
@@ -286,7 +202,7 @@ Language-agnostic registry-primary resolver. Replaces the Call-Resolution DAG fo
```
Orchestrator: `runScopeResolution(input, provider)` in `scope-resolution/pipeline/run.ts`.
Pipeline phase: `scopeResolutionPhase` in `scope-resolution/pipeline/phase.ts` — iterates `SCOPE_RESOLVERS ∩ MIGRATED_LANGUAGES`, reads per-file Trees from the parse phase's `scopeTreeCache`, disposes the cache at the end.
Pipeline phase: `scopeResolutionPhase` in `scope-resolution/pipeline/phase.ts` — iterates the registered `SCOPE_RESOLVERS` over the worker-serialized `ParsedFile`s. (Per-language `emitScopeCaptures` hooks may reuse a cached Tree via the orchestrator's `treeCache`, but in worker-pool runs that cache is empty — Trees can't cross MessageChannels — so they consume the pre-extracted `ParsedFile` instead; § Performance notes.)
### `ScopeResolver` contract
@@ -312,7 +228,6 @@ Single interface a language implements to plug into the pipeline. Contract fully
1. Implement `ScopeResolver` in `languages/<lang>/scope-resolver.ts`.
2. Add entry to `SCOPE_RESOLVERS` in `scope-resolution/pipeline/registry.ts`.
3. Add the language to `MIGRATED_LANGUAGES` in `registry-primary-flag.ts` when the shadow-harness corpus parity ≥ 99% fixtures / ≥ 98% corpus.
CI auto-discovers the set via `tsx`. No workflow edit required.
@@ -328,7 +243,6 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
| `scope-resolution/graph-bridge/*.ts` | CLI-local translation from resolved references → `KnowledgeGraph` edges |
| `scope-resolution/scope/*.ts` | Generic scope-chain walkers + namespace targets |
| `scope-resolution/workspace-index.ts` | Build-once O(1) lookup index |
| `registry-primary-flag.ts` | `MIGRATED_LANGUAGES` set + `isRegistryPrimary(lang)` |
| `languages/python/index.ts` | Python `ScopeResolver` hooks + known-limitation docs |
| `languages/python/captures.ts` | `emitPythonScopeCaptures` (honors cross-phase Tree cache) |
| `languages/csharp/index.ts` | C# `ScopeResolver` hooks + known-limitation docs |
@@ -337,7 +251,7 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
### Performance notes
- **Cross-phase Tree cache**: parse phase writes Trees into `scopeTreeCache` (separate from the chunk-local `astCache`) ONLY for languages with `emitScopeCaptures`. Scope-resolution reads from it to skip the second parse. Cleared at end of the phase. Workers leave the cache empty — Trees can't cross MessageChannels; cache miss = fresh parse. `PROF_SCOPE_RESOLUTION=1` emits hit/miss counters and a worker-engaged warning.
- **Cross-phase Tree cache**: the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) lets a scope-resolution per-language hook (`emitScopeCaptures`) reuse a tree instead of re-parsing. Workers leave it empty — Trees can't cross MessageChannels — so in normal (worker-pool) runs scope-resolution does NOT rely on it: workers serialize each file's `ParsedFile` (+ capture side-channel) and stream them in, so scope-resolution consumes the pre-extracted artifact rather than re-parsing on the main thread (§ Chunked parse-and-resolve). `PROF_SCOPE_RESOLUTION=1` emits hit/miss counters and a worker-engaged warning.
- **Typed relationship iteration**: heritage + MRO walk only the EXTENDS / IMPLEMENTS / HAS_METHOD edges via `iterRelationshipsByType`, not the full relationship map.
- **Workspace-resolution-index**: O(1) `findOwnedMember` / `findExportedDef` / `classScopeByDefId` built once per run.
- **SCC-ordered cross-file return-type propagation** (PR #1050): `propagateImportedReturnTypes` walks `indexes.sccs` in reverse-topological order (leaves first), so multi-hop alias chains like `models.User → service.user → app.user` collapse to the terminal class in a single linear pass. Within each importer, the source module's `typeBindings` is chain-followed BEFORE mirroring (so we mirror terminal types, not intermediate refs), and the importer's own `typeBindings` is chain-followed AFTER mirroring (so local `const x = importedFn()` resolves before downstream importers run). Cyclic SCCs reach a partial fixpoint within a single pass without iterating to convergence — see the `ts-circular` cross-file-binding fixture which only asserts pipeline-no-throw. PROF output (`PROF_SCOPE_RESOLUTION=1`) splits `finalize` from `propagate` so quadratic regressions in the chain-follow surface independently.
@@ -351,7 +265,7 @@ CI auto-discovers the set via `tsx`. No workflow edit required.
```
Unified Graph Schema (44 node types, 21 relationship types)
↑
Unified Resolution (3-tier name lookup + MRO walk)
Scope-Resolution Pipeline (registry lookup + 3-tier import resolution + MRO)
↑
Language Providers (import semantics, type config, export checker, MRO strategy)
↑
@@ -376,7 +290,7 @@ Each language implements `LanguageProvider` (`language-provider.ts`). Key fields
### Unified capture tags
Per-language tree-sitter queries use different AST node names but produce the **same semantic capture tags**: `@definition.class`, `@definition.function`, `@call.name`, `@import.source`, `@heritage.extends`. Downstream extraction needs no language branching. Defined in `tree-sitter-queries.ts`.
Per-language tree-sitter queries use different AST node names but produce the **same semantic capture tags**: `@definition.class`, `@definition.function`, `@call.name`, `@import.source`, `@reference.inherits`. Downstream extraction needs no language branching. Defined in `tree-sitter-queries.ts`.
### Import resolution
@@ -400,23 +314,26 @@ Unified 3-tier algorithm (`model/resolution-context.ts`), per-language `importSe
### Chunked parse-and-resolve
`parse` processes files in ~20 MB byte-budget chunks to bound memory. Per chunk:
1. Worker pool dispatches files (or sequential fallback via `skipWorkers`)
1. Worker pool dispatches files (the sole parse path — there is no sequential fallback; `skipWorkers`, `--workers 0`, and `GITNEXUS_WORKER_POOL_SIZE=0` are rejected with an actionable error)
2. Each worker: detect language → load grammar → run queries → return unified `ParseWorkerResult`
3. Synthesize wildcard bindings (`wildcard-synthesis.ts`)
4. Resolve imports and heritage
4. Resolve imports
5. Collect `BindingAccumulator` entries for cross-file propagation
Inheritance edges are emitted later, by the scope-resolution phase (`preEmitInheritanceEdges` + `emitHeritageEdges`), not during `parse`.
Workers: `workers/worker-pool.ts`, `workers/parse-worker.ts`.
### Heritage and MRO
**Worker-serialized ParsedFiles (#2038).** To index very large repos (e.g. the Linux kernel) without OOM, the worker pool is the *sole* parse path and workers serialize each file's `ParsedFile` (plus its capture side-channel) in parallel, streaming them to scope-resolution through a disk-backed store. Scope-resolution consumes the pre-extracted artifact instead of re-parsing every file on the main thread — tree-sitter's native input buffers are not GC-reclaimable, so the former main-thread re-parse leaked native memory until the process died. Pool creation is lazy / cache-miss-gated, so a warm all-cache-hit run replays cached worker output without spawning a worker (hence `usedWorkerPool` can be false even when the repo has parseable files).
All languages emit unified `ExtractedHeritage` (child, parent, `EXTENDS`/`IMPLEMENTS`). MRO phase walks the heritage graph using per-language strategy:
### Inheritance and MRO
Inheritance is captured by the `@reference.inherits` tag and emitted by the scope-resolution phase: `preEmitInheritanceEdges` resolves each base in scope, then `emitHeritageEdges` writes the `EXTENDS`/`IMPLEMENTS` edges. The phase then computes method resolution order via each `ScopeResolver`'s `buildMro` hook, feeding a `MethodDispatchIndex` used for owner-scoped lookups. Per-language strategy:
- **`first-wins`** — Java, C#, C++, TS, Ruby, Go
- **`c3`** — Python (C3 linearization)
- **`ruby-mixin`** — Ruby (mixin-aware linearization)
- **`none`** — single-inheritance languages
Unified walk: `lookupMethodByOwnerWithMRO()` in `model/resolve.ts`.
---
## Full analysis flow
+9 -9
View File
@@ -35,7 +35,7 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g
## Reference Documentation
- **This repository:** [AGENTS.md](AGENTS.md) (Cursor + monorepo notes), [ARCHITECTURE.md](ARCHITECTURE.md), [CONTRIBUTING.md](CONTRIBUTING.md), [GUARDRAILS.md](GUARDRAILS.md).
- **Call-resolution DAG:** See ARCHITECTURE.md § Call-Resolution DAG. Shared pipeline code in `gitnexus/src/core/ingestion/` must not name languages — use `LanguageProvider` hooks instead (see AGENTS.md).
- **Call & inheritance resolution:** See ARCHITECTURE.md § Scope-Resolution Pipeline. Shared pipeline code in `gitnexus/src/core/ingestion/` must not name languages — use `LanguageProvider` / `ScopeResolver` hooks instead (see AGENTS.md). (The legacy call-resolution DAG was removed in #942.)
- **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
@@ -58,22 +58,22 @@ See the `<!-- gitnexus:start --> … <!-- gitnexus:end -->` block in **[AGENTS.m
This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 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.
> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
## 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 run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `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 exploring unfamiliar code, use `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 `context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER edit a function, class, or method without first running `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.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
## Resources
+6
View File
@@ -13,11 +13,17 @@ This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformpro
## Development setup
**Prerequisites:** Node.js — `gitnexus/` requires `>=22.0.0` and `gitnexus-web/` requires `^20.19.0 || >=22.12.0` (enforced via the `engines` field in each package). Use `nvm install` to match the local version.
1. Clone the repository.
2. **CLI / MCP package:** `cd gitnexus && npm install && npm run build`
3. **Web UI (if needed):** `cd gitnexus-web && npm install`
4. Run tests as described in [TESTING.md](TESTING.md).
### Containerized development (optional)
If you prefer an isolated environment with Claude Code, OpenAI Codex CLI, and Cursor CLI pre-installed, open the repo in VS Code with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) and run **Dev Containers: Reopen in Container**. See [`.devcontainer/README.md`](.devcontainer/README.md) for first-time auth flows and Windows WSL2 setup.
## Branch and pull requests
- Use short-lived branches off the default branch of the repo you are targeting.
+100 -13
View File
@@ -22,6 +22,9 @@
<a href="https://securityscorecards.dev/viewer/?uri=github.com/abhigyanpatwari/GitNexus">
<img src="https://api.securityscorecards.dev/projects/github.com/abhigyanpatwari/GitNexus/badge" alt="OpenSSF Scorecard"/>
</a>
<a href="https://github.com/abhigyanpatwari/GitNexus/actions/workflows/ci.yml">
<img src="https://github.com/abhigyanpatwari/GitNexus/actions/workflows/ci.yml/badge.svg" alt="CI Workflows"/>
</a>
<p><strong>Enterprise (SaaS & Self-hosted)</strong> - <a href="https://akonlabs.com">akonlabs.com</a></p>
@@ -35,7 +38,7 @@ https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72
> _Like DeepWiki, but deeper._ DeepWiki helps you _understand_ code. GitNexus lets you _analyze_ it — because a knowledge graph tracks every relationship, not just descriptions.
**TL;DR:** The **Web UI** is a quick way to chat with any repo. The **CLI + MCP** is how you make your AI agent actually reliable — it gives Cursor, Claude Code, Codex, and friends a deep architectural view of your codebase so they stop missing dependencies, breaking call chains, and shipping blind edits. Even smaller models get full architectural clarity, making it compete with Goliath models.
**TL;DR:** The **Web UI** is a quick way to chat with any repo. The **CLI + MCP** is how you make your AI agent actually reliable — it gives Cursor, Claude Code, Antigravity, Codex, and friends a deep architectural view of your codebase so they stop missing dependencies, breaking call chains, and shipping blind edits. Even smaller models get full architectural clarity, making it compete with Goliath models.
---
@@ -48,7 +51,7 @@ https://github.com/user-attachments/assets/172685ba-8e54-4ea7-9ad1-e31a3398da72
| | **CLI + MCP** | **Web UI** |
| ----------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **What** | Index repos locally, connect AI agents via MCP | Visual graph explorer + AI chat in browser |
| **For** | Daily development with Cursor, Claude Code, Codex, Windsurf, OpenCode | Quick exploration, demos, one-off analysis |
| **For** | Daily development with Cursor, Claude Code, Antigravity, Codex, Windsurf, OpenCode | Quick exploration, demos, one-off analysis |
| **Scale** | Full repos, any size | Limited by browser memory (~5k files), or unlimited via backend mode |
| **Install** | `npm install -g gitnexus` | No install — [gitnexus.vercel.app](https://gitnexus.vercel.app) |
| **Storage** | LadybugDB native (fast, persistent) | LadybugDB WASM (in-memory, per session) |
@@ -104,6 +107,14 @@ npx gitnexus analyze
That's it. This indexes the codebase, installs agent skills, registers Claude Code hooks, and creates `AGENTS.md` / `CLAUDE.md` context files — all in one command.
> **On npm 11.x?** `npx` can crash during install with `Cannot destructure property 'package' of 'node.target'` (an npm/arborist bug, before GitNexus runs). Use pnpm instead — it builds the native deps explicitly:
>
> ```bash
> pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze
> ```
>
> Or install globally (`npm install -g gitnexus@latest`) and run `gitnexus analyze`. See [#1939](https://github.com/abhigyanpatwari/GitNexus/issues/1939).
To configure MCP for your editor, run `npx gitnexus setup` once — or set it up manually below.
> **Faster install (no C++ toolchain needed):** set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before `npm install -g gitnexus` to skip vendored grammar materialize/build (`tree-sitter-dart`, `tree-sitter-proto`, `tree-sitter-swift`). Dart/Proto/Swift files won't be parsed, but install completes in seconds without `python3`/`make`/`g++`. Strict `=1` only — any other value falls through to the rebuild.
@@ -114,16 +125,20 @@ To configure MCP for your editor, run `npx gitnexus setup` once — or set it up
### Editor Support
| Editor | MCP | Skills | Hooks (auto-augment) | Support |
| --------------- | --- | ------ | --------------------------------------------------------------------------------------- | ------------ |
| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** |
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
| **Codex** | Yes | Yes | — | MCP + Skills |
| **Windsurf** | Yes | — | — | MCP |
| **OpenCode** | Yes | Yes | — | MCP + Skills |
| Editor | MCP | Skills | Hooks (auto-augment) | Support |
| -------------------- | --- | ------ | --------------------------------------------------------------------------------------- | ------------ |
| **Claude Code** | Yes | Yes | Yes (PreToolUse + PostToolUse) | **Full** |
| **Cursor** | Yes | Yes | Yes (postToolUse, [manual install](gitnexus-cursor-integration/README.md#hook-install)) | **Full** |
| **Antigravity** (Google) | Yes | Yes | Yes (AfterTool, [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/))[¹](#fn-antigravity-hooks) | **Full** |
| **Codex** | Yes | Yes | — | MCP + Skills |
| **Windsurf** | Yes | — | — | MCP |
| **OpenCode** | Yes | Yes | — | MCP + Skills |
> **Claude Code** gets the deepest integration: MCP tools + agent skills + PreToolUse hooks that enrich searches with graph context + PostToolUse hooks that detect a stale index after commits and prompt the agent to reindex.
<a id="fn-antigravity-hooks"></a>
> ¹ **Antigravity hooks** follow the [Gemini CLI hooks reference](https://geminicli.com/docs/hooks/reference/) (Antigravity 2.0 is the documented successor to Gemini CLI). Augmentation runs in `AfterTool` because `BeforeTool` has no context-injection channel in the Gemini contract — the agent sees graph context appended to the tool result via `hookSpecificOutput.additionalContext`. Stale-index hints land in the same channel after a successful `git commit/merge/rebase/cherry-pick/pull`. The schema may evolve if Antigravity-specific hook docs diverge from Gemini CLI's; the implementation will track those changes.
## Community Integrations
Built by the community — not officially maintained, but worth checking out.
@@ -168,6 +183,21 @@ codex mcp add gitnexus -- npx -y gitnexus@latest mcp
}
```
**Antigravity** (Google) — `~/.gemini/antigravity/mcp_config.json`:
```json
{
"mcpServers": {
"gitnexus": {
"command": "npx",
"args": ["-y", "gitnexus@latest", "mcp"]
}
}
}
```
> `gitnexus setup` also merges an `AfterTool` entry into `~/.gemini/settings.json` (under the canonical [Gemini CLI hooks schema](https://geminicli.com/docs/hooks/reference/)) and installs skills to `~/.gemini/antigravity/skills/`. Existing user hooks are preserved. The hook adapter's path is rewritten at install time, so run `gitnexus setup` rather than hand-editing.
**OpenCode** (`~/.config/opencode/config.json`):
```json
@@ -199,12 +229,14 @@ gitnexus analyze --force # Full rebuild: re-parse + graph rebuild + FTS
gitnexus analyze --skills # Generate repo-specific skill files from detected communities
gitnexus analyze --skip-embeddings # Skip embedding generation (faster)
gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexus section edits
gitnexus analyze --skip-skills # Skip installing .claude/skills/gitnexus/ skill files
gitnexus analyze --default-branch develop # Branch used in the generated regression-compare example (base_ref)
gitnexus analyze --skip-git # Index folders that are not Git repositories
gitnexus analyze --embeddings # Enable embedding generation (slower, better search)
gitnexus analyze --embeddings [limit] # Enable embedding generation (slower, better search)
gitnexus analyze --verbose # Log skipped files when parsers are unavailable
gitnexus analyze --worker-timeout 60 # Increase worker idle timeout for slow parses
gitnexus analyze --wal-checkpoint-threshold 67108864 # 64 MiB. Control LadybugDB WAL auto-checkpoint threshold (default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)
gitnexus analyze --workers <n> # Parse worker pool size (default: cores-1, capped at 16; 0 = sequential)
gitnexus analyze --workers <n> # Parse worker pool size (>=1; default: cores-1, capped at 16, auto-sized to the repo). 0 is rejected — there is no sequential mode.
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
gitnexus serve # Start local HTTP server (multi-repo) for web UI connection
gitnexus list # List all indexed repositories
@@ -229,13 +261,60 @@ gitnexus group status <name> # Check staleness of repos in a group
If `analyze` reports a worker parse timeout on a large or unusual repository, it keeps running and falls back safely. To give slow worker jobs more time, use `gitnexus analyze --worker-timeout 60` or set `GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=60000`. For very large files, `GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES` controls the worker job byte budget.
#### Embeddings node limit
`gitnexus analyze --embeddings` generates semantic search vectors with a default 50,000-node safety cap to protect memory on large repositories. Override the cap when you know the host has enough memory for a larger graph, or disable it entirely for a one-off full embeddings run.
```bash
# Generate embeddings with the default 50,000 node safety cap
gitnexus analyze --embeddings
# Disable the safety cap entirely
gitnexus analyze --embeddings 0
# Use a custom cap
gitnexus analyze --embeddings 100000
```
If embeddings are skipped on a large repository, the indexed graph likely exceeds the default safety cap. Re-run with `gitnexus analyze --embeddings 0` to remove the cap, or `gitnexus analyze --embeddings <n>` to choose a higher limit while still keeping memory bounded.
#### Project config (`.gitnexusrc`)
Commit a `.gitnexusrc` JSON file at the repo root to preconfigure recurring `analyze` options per project, instead of re-passing the same flags every run. It is read from the resolved repo root (not `.gitnexus/`, which is gitignored index storage). **CLI flags always override `.gitnexusrc`.**
```jsonc
{
// Default branch used in the generated regression-compare example (base_ref).
// Use this so a project on `develop`/`master` doesn't get "main" rewritten
// over its fix on every analyze. (Alias: "branch".)
"defaultBranch": "develop",
"skipContextFiles": true, // alias of skipAgentsMd: keep your own AGENTS.md/CLAUDE.md
"skipSkills": true, // don't install .claude/skills/gitnexus/
"embeddings": true, // generate embeddings by default
"workerTimeout": 60
}
```
A nested `analyze` block is also accepted (and overrides flat keys for the same option):
```json
{ "analyze": { "defaultBranch": "develop", "skipSkills": true } }
```
Notes:
- The default branch is resolved as: `--default-branch` > `.gitnexusrc` `defaultBranch`/`branch` > auto-detected `origin/HEAD` > `main`.
- `skipContextFiles` / `skipAiContext` are aliases for `skipAgentsMd` — they skip the `AGENTS.md` / `CLAUDE.md` block only. They do **not** imply `skipSkills`. `indexOnly` is the stronger option that skips all file injection.
- Supported keys: `defaultBranch` (`branch`), `skipAgentsMd` (`skipContextFiles`, `skipAiContext`), `skipSkills`, `indexOnly`, `stats`/`noStats`, `embeddings`, `dropEmbeddings`, `name`, `allowDuplicateName`, `maxFileSize`, `workerTimeout`, `walCheckpointThreshold`, `workers`, `embeddingThreads`, `embeddingBatchSize`, `embeddingSubBatchSize`, `embeddingDevice`.
- The file is JSON only. Unknown keys and invalid values fail fast with an actionable error before analysis starts.
#### Environment variables
Most `analyze` knobs are also CLI flags (`--workers`, `--worker-timeout`, `--max-file-size`, `--verbose`). Use the env-var form when you'd otherwise repeat the same flag every run, or when invoking GitNexus from a long-running host (MCP server, eval-server, CI shell) that already manages its own environment. CLI flags take precedence over env vars; env vars take precedence over built-in defaults.
| Variable | Default | Effect | Tune when… |
| -------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `GITNEXUS_WORKER_POOL_SIZE` | `cores - 1`, capped at 16 | Parse worker pool size. `0` disables the pool (sequential fallback). Equivalent to `--workers <n>`. | Constrained containers (cgroup CPU limits), CI runners with explicit quotas, or debugging a worker-only crash via `0`. |
| `GITNEXUS_WORKER_POOL_SIZE` | `cores - 1`, capped at 16 | Parse worker pool size (must be ≥ 1). Equivalent to `--workers <n>`. The worker pool is the sole parse path — there is no sequential parser, so `0` is rejected with an actionable error (the pool self-heals via quarantine + respawn). | Constrained containers (cgroup CPU limits) or CI runners with explicit quotas. To narrow down a worker crash set `1` for a single-worker pool — not `0`. |
| `GITNEXUS_PARSE_CHUNK_CONCURRENCY` | `2` | Number of chunks whose file contents may be read into memory in parallel while the pool dispatches the current chunk. Worker dispatch itself stays serial. | Repos large enough to chunk (multi-MB total source) where disk I/O is a measurable fraction of analyze wall-clock. |
| `GITNEXUS_VERBOSE` | unset | When `1`, enables verbose ingestion logs (skipped-file warnings, per-chunk throughput, parse-cache stats). Equivalent to `--verbose`. | Debugging an analyze that "completed" but seems to have missed files; tuning `--workers` / chunk concurrency against observable throughput. |
| `GITNEXUS_PROFILE_DEFERRED` | unset | When `1`, emits `[deferred-profile]` timing/progress logs for the post-chunk deferred resolution band (imports → heritage → buildHeritageMap → legacy call resolution). Implied by `GITNEXUS_VERBOSE`. | Diagnosing analyze stalls in "Resolving calls (all chunks)" on large Java/Kotlin repos (issue #1741) without the full verbose ingestion noise. |
@@ -639,7 +718,15 @@ UPSTREAM (what depends on this):
authRouter [IMPORTS] -> src/routes/auth.ts
```
Options: `maxDepth`, `minConfidence`, `relationTypes` (`CALLS`, `IMPORTS`, `EXTENDS`, `IMPLEMENTS`), `includeTests`
Options: `maxDepth`, `minConfidence`, `relationTypes` (`CALLS`, `IMPORTS`, `EXTENDS`, `IMPLEMENTS`), `includeTests`, `limit` (max symbols per depth, default 100), `offset` (pagination start per depth), `summaryOnly` (counts and risk only, omits symbol list)
**Disambiguation** — when several symbols share the target name, `impact` returns a ranked `ambiguous` candidate list instead of guessing. Narrow it with `target_uid` (exact, zero-ambiguity), `file_path`, or `kind` (`Function`, `Class`, `Method`, …). From the CLI these are `--uid`, `--file`, and `--kind`, matching `gitnexus context`:
```bash
gitnexus impact get_embeddings # → ambiguous: lists ranked candidates
gitnexus impact get_embeddings --file src/embed.py # → resolves to the one in that file
gitnexus impact get_embeddings --uid "Function:src/embed.py:get_embeddings" # exact
```
### Process-Grouped Search
+32 -42
View File
@@ -4,11 +4,11 @@ How we structure tests and which commands to run locally and in CI.
## Packages
| Package | Path | Runner | Notes |
| -------------- | -------------- | -------- | ------------------------------ |
| CLI + MCP core | `gitnexus/` | Vitest | Primary test surface in CI |
| Web UI | `gitnexus-web/`| Vitest | Unit/component tests |
| Web UI E2E | `gitnexus-web/`| Playwright | Run when changing UI flows |
| Package | Path | Runner | Notes |
| -------------- | --------------- | ---------- | -------------------------- |
| CLI + MCP core | `gitnexus/` | Vitest | Primary test surface in CI |
| Web UI | `gitnexus-web/` | Vitest | Unit/component tests |
| Web UI E2E | `gitnexus-web/` | Playwright | Run when changing UI flows |
## Test lanes
@@ -16,25 +16,25 @@ How we structure tests and which commands to run locally and in CI.
From `gitnexus/`:
| Command | What it runs | When to use |
| ------------------------ | ---------------------------------------------------- | ------------------------------- |
| `npm test` | Full suite (all 3 vitest projects) | Before opening a PR |
| `npm run test:unit` | Unit tests only (`test/unit/`) | Tight development loop |
| `npm run test:integration` | Integration tests (`test/integration/`) | After changing pipelines, DB, workers |
| `npm run test:coverage` | Full suite + v8 coverage with thresholds | Checking coverage impact |
| `npm run test:parity` | Scope-resolution parity for all migrated languages | After changing resolver or scope code |
| `npm run test:cross-platform` | Platform-sensitive subset only | Debugging a Windows/macOS issue |
| `npm run test:watch` | Vitest in watch mode | Active development |
| Command | What it runs | When to use |
| ----------------------------- | -------------------------------------------------- | ------------------------------------- |
| `npm test` | Full suite (all 3 vitest projects) | Before opening a PR |
| `npm run test:unit` | Unit tests only (`test/unit/`) | Tight development loop |
| `npm run test:integration` | Integration tests (`test/integration/`) | After changing pipelines, DB, workers |
| `npm run test:coverage` | Full suite + v8 coverage with thresholds | Checking coverage impact |
| `npm run test:parity` | Scope-resolution parity for all migrated languages | After changing resolver or scope code |
| `npm run test:cross-platform` | Platform-sensitive subset only | Debugging a Windows/macOS issue |
| `npm run test:watch` | Vitest in watch mode | Active development |
### `gitnexus-web/` commands
From `gitnexus-web/`:
| Command | What it runs | When to use |
| ---------------------- | --------------------------------- | ------------------------------ |
| `npm test` | Unit/component tests (vitest) | After changing web code |
| `npm run test:coverage`| Unit tests + coverage | Checking coverage impact |
| `npm run test:e2e` | Playwright browser tests | After changing UI flows (requires `gitnexus serve` + `npm run dev`) |
| Command | What it runs | When to use |
| ----------------------- | ----------------------------- | ------------------------------------------------------------------- |
| `npm test` | Unit/component tests (vitest) | After changing web code |
| `npm run test:coverage` | Unit tests + coverage | Checking coverage impact |
| `npm run test:e2e` | Playwright browser tests | After changing UI flows (requires `gitnexus serve` + `npm run dev`) |
### Before opening a PR
@@ -59,11 +59,11 @@ Skip with `git commit --no-verify` (use sparingly).
`gitnexus/vitest.config.ts` defines three projects for safety isolation:
| Project | Files | Parallelism | Purpose |
| ---------- | ----------------------------- | ----------- | ---------------------------------------------- |
| `lbug-db` | Native LadybugDB integration tests (explicit list) | Sequential | Prevents file-lock conflicts from native mmap addon |
| `cli-e2e` | `skills-e2e.test.ts` | Sequential | CLI process spawning requires serial execution |
| `default` | Everything else | Parallel | Fast execution for pure logic and parser tests |
| Project | Files | Parallelism | Purpose |
| --------- | -------------------------------------------------- | ----------- | --------------------------------------------------- |
| `lbug-db` | Native LadybugDB integration tests (explicit list) | Sequential | Prevents file-lock conflicts from native mmap addon |
| `cli-e2e` | `skills-e2e.test.ts` | Sequential | CLI process spawning requires serial execution |
| `default` | Everything else | Parallel | Fast execution for pure logic and parser tests |
When adding a new test that uses native LadybugDB (`@ladybugdb/core`), add it to the `lbug-db` project's explicit include list and the `default` project's exclude list.
@@ -74,21 +74,11 @@ When adding a new test that uses native LadybugDB (`@ladybugdb/core`), add it to
- **Resolver / parity** — Language-specific call-resolution tests in `test/integration/resolvers/`.
- **E2E (web)** — Critical user paths only; prefer `data-testid` attributes for stable selectors. Tests run against real backend (`gitnexus serve`) and Vite dev server.
## Scope-resolution parity
## Scope-resolution tests
Migrated languages (listed in `MIGRATED_LANGUAGES` in `src/core/ingestion/registry-primary-flag.ts`) are tested in both legacy and registry-primary modes on every PR.
Every language resolves calls and inheritance through the scope-resolution pipeline — the legacy call-resolution DAG and the per-language `REGISTRY_PRIMARY_<LANG>` flag were removed in RING4-1 (#942). Each language's resolver test lives at `test/integration/resolvers/<slug>.test.ts` and runs once, on the single scope-resolution path, as part of the normal `tests` job (`vitest test/**/*.test.ts`).
For each migrated language, CI runs the resolver test file twice:
1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path
2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path
Both must pass. Known legacy gaps are listed in `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES` in `test/integration/resolvers/helpers.ts` and are automatically skipped in legacy mode.
Adding a language to `MIGRATED_LANGUAGES` automatically enrolls it in parity — no workflow or config edit needed. The test file must exist at `test/integration/resolvers/<slug>.test.ts`.
Run parity locally: `cd gitnexus && npm run test:parity`
Run for a single language: `cd gitnexus && npx tsx scripts/run-parity.ts --language python`
Adding a language: register its `ScopeResolver` in `scope-resolution/pipeline/registry.ts` (`SCOPE_RESOLVERS`) and add the resolver test file — no workflow or config edit needed.
## Cross-platform testing
@@ -120,12 +110,12 @@ To check the cross-platform list is up to date, run `npm run test:cross-platform
GitHub Actions (`.github/workflows/ci.yml`) orchestrate:
| Workflow | Jobs | Purpose |
| --------------------- | ------------------------------ | ------------------------------------------------ |
| `ci-quality.yml` | format, lint, typecheck, typecheck-web, workflow-convention | Code quality gates |
| Workflow | Jobs | Purpose |
| --------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
| `ci-quality.yml` | format, lint, typecheck, typecheck-web, workflow-convention | Code quality gates |
| `ci-tests.yml` | ubuntu/coverage, cross-platform (Win/Mac), packaged-install-smoke | Full suite + coverage on Ubuntu; platform-sensitive subset on Win/Mac |
| `ci-scope-parity.yml` | discover, parity | Scope-resolution parity for all migrated languages |
| `ci-e2e.yml` | e2e (chromium) | Playwright E2E, gated on `gitnexus-web/**` changes |
| `ci-scope-parity.yml` | discover, parity | Scope-resolution parity for all migrated languages |
| `ci-e2e.yml` | e2e (chromium) | Playwright E2E, gated on `gitnexus-web/**` changes |
The `CI Gate` job in `ci.yml` is the single required check for branch protection. It requires quality, tests, e2e, and scope-parity to all pass.
+6
View File
@@ -30,6 +30,12 @@ services:
container_name: ${WEB_CONTAINER_NAME:-gitnexus-web}
ports:
- '${WEB_HOST_PORT:-4173}:4173'
# Override the backend URL served to the browser. The default
# (http://localhost:4747) works when both containers run locally.
# Set GITNEXUS_BACKEND_URL in your .env or shell for remote/custom setups:
# GITNEXUS_BACKEND_URL=http://<server-ip>:4747
environment:
- GITNEXUS_BACKEND_URL=${GITNEXUS_BACKEND_URL:-}
depends_on:
gitnexus-server:
condition: service_healthy
+116 -61
View File
@@ -1,12 +1,39 @@
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { open } from 'node:fs/promises';
import { createServer } from 'node:http';
import { extname, isAbsolute, normalize, relative, resolve } from 'node:path';
import { extname, isAbsolute, normalize, relative, resolve, sep } from 'node:path';
const host = '0.0.0.0';
const port = Number(process.env.PORT || '4173');
const root = resolve(process.cwd(), 'dist');
function isValidUrl(value) {
try {
const u = new URL(value);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
}
function jsonForScriptTag(obj) {
return JSON.stringify(obj)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
}
const rawBackendUrl = process.env.GITNEXUS_BACKEND_URL ?? null;
if (rawBackendUrl && !isValidUrl(rawBackendUrl)) {
const safeRaw = rawBackendUrl.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 200);
console.warn(
`[gitnexus-web] GITNEXUS_BACKEND_URL "${safeRaw}" is not a valid http/https URL -- ignoring.`,
);
}
const backendUrl = rawBackendUrl && isValidUrl(rawBackendUrl) ? rawBackendUrl : null;
const configScript = backendUrl
? `<script>window.__GITNEXUS_CONFIG__=${jsonForScriptTag({ backendUrl })};</script>`
: '';
const contentTypes = {
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
@@ -22,22 +49,22 @@ const contentTypes = {
// Static asset server for the gitnexus-web Docker image.
//
// Path-injection containment: the request handler is intentionally a single
// inline pipeline with no helper functions on the path-data flow. Each
// filesystem sink (stat, createReadStream) is immediately preceded by the
// canonical `path.relative` containment check that CodeQL's
// `js/path-injection` query recognizes as a sanitizer barrier:
// TOCTOU prevention: every filesystem interaction uses open() to get a
// file handle; subsequent reads use handle.readFile()/createReadStream().
//
// const rel = relative(root, candidate);
// if (rel.startsWith('..') || isAbsolute(rel)) reject;
// // candidate is now proven inside `root`
// CodeQL js/file-system-race: the query pairs open() calls when their
// path arguments are data-flow aliased. This handler uses exactly two
// open() calls whose paths are provably independent:
// 1. open(requestedPath) — derived from the URL
// 2. open(spaFallback) — the constant root/index.html
// Because spaFallback has no data-flow from the request, CodeQL cannot
// pair them as a check/use on the same path.
//
// Earlier iterations of this file used a helper (`resolveWithinRoot`) and a
// `startsWith(root + sep)` check. Both were semantically correct but neither
// was recognized by CodeQL: `startsWith(root + sep)` is not in the analyzer's
// barrier-pattern set, and helper-based sanitization is not followed across
// the request handler's reassignment paths in vanilla JS. The inline-at-sink
// shape below is the documented analyzer-friendly idiom.
// Path-injection containment: each open() is preceded by a
// path.relative() barrier that CodeQL recognizes as a sanitizer.
const spaFallback = resolve(root, 'index.html');
const server = createServer(async (req, res) => {
const urlPath = req.url?.split('?')[0] || '/';
@@ -56,62 +83,90 @@ const server = createServer(async (req, res) => {
}
const cleanPath = normalize(decoded.replace(/^\/+/, ''));
const initialPath = resolve(root, cleanPath);
const requestedPath = resolve(root, cleanPath);
// Sanitizer barrier #1 — guards the first stat() sink.
const initialRel = relative(root, initialPath);
if (initialRel.startsWith('..') || isAbsolute(initialRel)) {
const rel = relative(root, requestedPath);
if (rel.startsWith('..') || isAbsolute(rel)) {
res.writeHead(400);
res.end('Bad request');
return;
}
let handle;
try {
const initialStat = await stat(initialPath).catch(() => null);
let servePath = requestedPath;
// Pick the path we actually serve. Note: any branch reassigns to a
// freshly-resolved path; the next sanitizer barrier re-validates.
let finalPath;
if (initialStat?.isDirectory()) {
finalPath = resolve(initialPath, 'index.html');
} else if (!initialStat?.isFile()) {
finalPath = resolve(root, 'index.html');
// Try to open the exact path the client asked for.
handle = await open(requestedPath, 'r').catch(() => null);
if (handle) {
const s = await handle.stat();
if (!s.isFile()) {
// Directories and other non-files fall through to SPA fallback.
await handle.close();
handle = null;
}
}
// If the requested path wasn't a regular file, serve the SPA entry
// point. spaFallback is a module-level constant with no data-flow
// from the request, so this open() is independent of the one above.
if (!handle) {
servePath = spaFallback;
handle = await open(spaFallback, 'r').catch(() => null);
if (!handle) {
res.writeHead(404);
res.end('Not found');
return;
}
const s = await handle.stat();
if (!s.isFile()) {
res.writeHead(404);
res.end('Not found');
return;
}
}
const isHtml = extname(servePath) === '.html' || !extname(servePath);
const cacheControl = servePath.includes(`${sep}assets${sep}`)
? 'public, max-age=31536000, immutable'
: 'no-cache';
const contentType = contentTypes[extname(servePath)] || 'application/octet-stream';
if (isHtml && configScript) {
const raw = await handle.readFile('utf8');
await handle.close();
handle = null;
if (!raw.includes('</head>')) {
console.warn('[gitnexus-web] Could not inject config: no </head> tag found in HTML');
}
const html = raw.includes('</head>') ? raw.replace('</head>', `${configScript}</head>`) : raw;
const buf = Buffer.from(html, 'utf8');
res.writeHead(200, {
'Cache-Control': cacheControl,
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': buf.length,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
});
res.end(buf);
} else {
finalPath = initialPath;
res.writeHead(200, {
'Cache-Control': cacheControl,
'Content-Type': contentType,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
});
const stream = handle.createReadStream();
handle = null;
stream.on('error', () => res.destroy());
stream.pipe(res);
}
// Sanitizer barrier #2 — guards both the second stat() and the
// createReadStream() sinks. No reassignment of finalPath happens
// between this guard and either sink, so the analyzer can prove
// containment for both.
const finalRel = relative(root, finalPath);
if (finalRel.startsWith('..') || isAbsolute(finalRel)) {
res.writeHead(400);
res.end('Bad request');
return;
}
const finalStat = await stat(finalPath).catch(() => null);
if (!finalStat?.isFile()) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, {
'Cache-Control': finalPath.includes('/assets/')
? 'public, max-age=31536000, immutable'
: 'no-cache',
'Content-Type': contentTypes[extname(finalPath)] || 'application/octet-stream',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
});
const stream = createReadStream(finalPath);
stream.on('error', () => res.destroy());
stream.pipe(res);
} catch (error) {
console.error(error);
res.writeHead(500);
res.end(error instanceof Error ? error.message : 'Internal server error');
res.end('Internal server error');
} finally {
if (handle) await handle.close().catch(() => {});
}
});
+142 -1
View File
@@ -70,8 +70,20 @@ before(async () => {
await waitForServer(serverPort);
});
function killAndWait(proc) {
return new Promise((resolve) => {
if (!proc || proc.exitCode !== null) {
resolve();
return;
}
proc.once('exit', resolve);
proc.kill();
if (proc.exitCode !== null) resolve();
});
}
after(async () => {
child?.kill();
await killAndWait(child);
if (tmpDir) await rm(tmpDir, { recursive: true, force: true });
});
@@ -122,3 +134,132 @@ it('returns 404 when dist/index.html is missing', async () => {
const res = await rawGet(serverPort, '/nonexistent-page');
assert.equal(res.status, 404);
});
// -- Config injection: server-level integration tests ---
function spawnServerWithEnv(cwd, port, env) {
const proc = spawn(process.execPath, [serverScript], {
cwd,
env: { ...process.env, PORT: String(port), ...env },
stdio: 'pipe',
});
proc.on('error', (err) => {
throw err;
});
return proc;
}
async function withInjectionServer(envOverrides, fn) {
const dir = await mkdtemp(join(tmpdir(), 'gitnexus-inject-'));
const distDir = join(dir, 'dist');
const assetsDir = join(distDir, 'assets');
await mkdir(assetsDir, { recursive: true });
await writeFile(
join(distDir, 'index.html'),
'<!doctype html><html><head><meta charset="utf-8"></head><body>app</body></html>',
);
await writeFile(join(assetsDir, 'style.abc.css'), 'body{}');
const port = await getFreePort();
const proc = spawnServerWithEnv(dir, port, envOverrides);
try {
await waitForServer(port);
await fn(port);
} finally {
await killAndWait(proc);
await rm(dir, { recursive: true, force: true });
}
}
it('injects __GITNEXUS_CONFIG__ into / when GITNEXUS_BACKEND_URL is valid', async () => {
await withInjectionServer({ GITNEXUS_BACKEND_URL: 'http://10.0.0.1:4747' }, async (port) => {
const res = await rawGet(port, '/');
assert.equal(res.status, 200);
assert.ok(
res.body.includes('window.__GITNEXUS_CONFIG__'),
'Expected __GITNEXUS_CONFIG__ in response body',
);
assert.ok(res.body.includes('http://10.0.0.1:4747'), 'Expected backend URL in response body');
});
});
it('injects __GITNEXUS_CONFIG__ into SPA fallback routes', async () => {
await withInjectionServer({ GITNEXUS_BACKEND_URL: 'http://10.0.0.1:4747' }, async (port) => {
const res = await rawGet(port, '/some/deep/link');
assert.equal(res.status, 200);
assert.ok(
res.body.includes('window.__GITNEXUS_CONFIG__'),
'Expected __GITNEXUS_CONFIG__ in SPA fallback response',
);
assert.ok(
res.body.includes('http://10.0.0.1:4747'),
'Expected backend URL in SPA fallback response',
);
});
});
it('does not inject when GITNEXUS_BACKEND_URL is not set', async () => {
await withInjectionServer({}, async (port) => {
const res = await rawGet(port, '/');
assert.equal(res.status, 200);
assert.ok(
!res.body.includes('__GITNEXUS_CONFIG__'),
'Expected no __GITNEXUS_CONFIG__ when env var is unset',
);
});
});
it('does not inject when GITNEXUS_BACKEND_URL is invalid', async () => {
await withInjectionServer({ GITNEXUS_BACKEND_URL: 'not-a-url' }, async (port) => {
const res = await rawGet(port, '/');
assert.equal(res.status, 200);
assert.ok(
!res.body.includes('__GITNEXUS_CONFIG__'),
'Expected no __GITNEXUS_CONFIG__ for invalid URL',
);
});
});
it('does not inject when GITNEXUS_BACKEND_URL uses a non-http protocol', async () => {
await withInjectionServer({ GITNEXUS_BACKEND_URL: 'ftp://somehost:21' }, async (port) => {
const res = await rawGet(port, '/');
assert.equal(res.status, 200);
assert.ok(
!res.body.includes('__GITNEXUS_CONFIG__'),
'Expected no __GITNEXUS_CONFIG__ for non-http protocol',
);
});
});
it('escapes </script> in GITNEXUS_BACKEND_URL to prevent XSS', async () => {
const xssUrl = 'http://example.com/?x=</script><script>alert(1)</script>';
await withInjectionServer({ GITNEXUS_BACKEND_URL: xssUrl }, async (port) => {
const res = await rawGet(port, '/');
assert.equal(res.status, 200);
const scriptMatches = res.body.match(/<script>/gi) || [];
assert.equal(
scriptMatches.length,
1,
`Expected exactly 1 <script> tag but found ${scriptMatches.length}: XSS breakout detected`,
);
assert.ok(
!res.body.includes('</script><script>'),
'</script> must not appear unescaped -- would allow script breakout',
);
assert.ok(res.body.includes('\\u003c'), 'Angle brackets must be escaped as \\u003c');
});
});
it('does not inject config into static assets', async () => {
await withInjectionServer({ GITNEXUS_BACKEND_URL: 'http://10.0.0.1:4747' }, async (port) => {
const res = await rawGet(port, '/assets/style.abc.css');
assert.equal(res.status, 200);
assert.ok(
!res.body.includes('__GITNEXUS_CONFIG__'),
'Static assets must not contain injected config',
);
assert.equal(res.body, 'body{}');
});
});
Generated
+106 -90
View File
@@ -21,7 +21,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.13.4"
version = "3.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -30,95 +30,111 @@ dependencies = [
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" },
{ url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" },
{ url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" },
{ url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" },
{ url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" },
{ url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" },
{ url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" },
{ url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" },
{ url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" },
{ url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" },
{ url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" },
{ url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" },
{ url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" },
{ url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" },
{ url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" },
{ url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" },
{ url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" },
{ url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" },
{ url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" },
{ url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" },
{ url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" },
{ url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" },
{ url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" },
{ url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" },
{ url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" },
{ url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" },
{ url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" },
{ url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" },
{ url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" },
{ url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" },
{ url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" },
{ url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" },
{ url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" },
{ url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" },
{ url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" },
{ url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" },
{ url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" },
{ url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" },
{ url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" },
{ url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" },
{ url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" },
{ url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" },
{ url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" },
{ url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" },
{ url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" },
{ url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" },
{ url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" },
{ url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" },
{ url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" },
{ url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" },
{ url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" },
{ url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" },
{ url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" },
{ url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" },
{ url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" },
{ url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" },
{ url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" },
{ url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" },
{ url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" },
{ url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" },
{ url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" },
{ url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" },
{ url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" },
{ url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" },
{ url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" },
{ url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" },
{ url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" },
{ url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" },
{ url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" },
{ url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" },
{ url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" },
{ url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" },
{ url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" },
{ url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" },
{ url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" },
{ url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" },
{ url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" },
{ url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" },
{ url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" },
{ url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" },
{ url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" },
{ url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" },
{ url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" },
{ url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" },
{ url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" },
{ url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" },
{ url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" },
{ url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" },
{ url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" },
{ url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" },
{ url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" },
{ url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" },
{ url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" },
{ url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" },
{ url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" },
{ url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" },
{ url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" },
{ url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" },
{ url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" },
{ url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" },
{ url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" },
{ url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" },
{ url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" },
{ url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" },
{ url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" },
{ url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" },
{ url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" },
{ url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" },
{ url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" },
{ url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" },
{ url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" },
{ url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" },
{ url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" },
{ url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" },
{ url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" },
{ url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" },
{ url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" },
{ url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" },
{ url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" },
{ url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" },
{ url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" },
{ url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" },
{ url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" },
{ url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" },
{ url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" },
{ url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" },
{ url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" },
{ url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" },
{ url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" },
{ url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" },
{ url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" },
{ url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" },
{ url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" },
{ url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" },
{ url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" },
{ url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" },
{ url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" },
{ url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" },
{ url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" },
{ url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" },
{ url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" },
{ url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" },
{ url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" },
{ url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" },
{ url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" },
{ url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" },
{ url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" },
{ url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" },
{ url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" },
{ url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" },
{ url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" },
{ url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" },
]
[[package]]
@@ -926,7 +942,7 @@ wheels = [
[[package]]
name = "litellm"
version = "1.83.14"
version = "1.87.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -942,9 +958,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "tokenizers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" }
sdist = { url = "https://files.pythonhosted.org/packages/77/0d/ccdf682ccfd7f18bf0e179c39d85616b8f8ef05a798588285310412db13d/litellm-1.87.0.tar.gz", hash = "sha256:cafc1882cb0cbab8374c41180af86e4a067796e4524e15f59e99f6e689cd1bd8", size = 15453755, upload-time = "2026-06-02T03:53:29.076Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" },
{ url = "https://files.pythonhosted.org/packages/98/20/88a372fa7e50fc2c33458c6eef94a79afcf7bdfa43610079531b82b484a3/litellm-1.87.0-py3-none-any.whl", hash = "sha256:fbbba7e47ae29b55f878fe1acc80effb92761bc168f6236bd81a0cb6e147d855", size = 17103948, upload-time = "2026-06-02T03:53:25.677Z" },
]
[[package]]
@@ -16,6 +16,7 @@ const path = require('path');
const { spawnSync } = require('child_process');
const { acquireHookSlot } = require('./hook-lock.js');
const { hasGitNexusDbLockedByGitNexusServer } = require('./hook-db-lock-probe.cjs');
const { formatAnalyzeCommand } = require('./resolve-analyze-cmd.cjs');
/**
* Read JSON input from stdin synchronously.
@@ -345,7 +346,7 @@ function handlePostToolUse(input) {
// If HEAD matches last indexed commit, no reindex needed
if (currentHead && currentHead === lastCommit) return;
const analyzeCmd = `npx gitnexus analyze${hadEmbeddings ? ' --embeddings' : ''}`;
const analyzeCmd = formatAnalyzeCommand({ embeddings: hadEmbeddings });
sendHookResponse(
'PostToolUse',
`GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` +
@@ -0,0 +1,323 @@
/**
* Single source of truth for how docs, hooks, and warnings invoke gitnexus.
*
* Automatically selects a working invocation path:
* 1. Global `gitnexus` on PATH (best — no install step)
* 2. npm 11+ with pnpm on PATH → `pnpm --allow-build=… dlx` (avoids the npx
* arborist crash *and* pnpm 10+ ignored-build-script failures, #1939)
* 3. npm < 11 with npm on PATH → `npx` (works; simpler than pnpm dlx)
* 4. pnpm-only → `pnpm --allow-build=… dlx`
* 5. Last resort → `npx` (warned on npm 11+ from analyze.ts)
*
* The `--allow-build` flags MUST precede the `dlx` token. pnpm < 10.14 keeps
* `dlx` in its argv escape list, so flags placed *after* `dlx` are parsed as
* package specs (ERR_PNPM_SPEC_NOT_SUPPORTED). The pre-`dlx` position parses
* into dlx's allow-build option and has been honored since pnpm 10.2.0 (#1939).
*
* This stays self-contained CJS because the Claude/Antigravity hooks run as
* standalone files copied into the user's hook dir, where no package import is
* available. The CLI reuses this module from src/cli/resolve-invocation.ts via
* createRequire rather than re-implementing it. Two committed copies must stay
* byte-identical (enforced by resolve-invocation.test.ts) — edit both together:
* gitnexus/hooks/claude/ (the canonical copy the CLI and `gitnexus setup` read)
* and gitnexus-claude-plugin/hooks/. A THIRD copy is written at runtime to
* `<repo>/.gitnexus/run.cjs` by `gitnexus analyze` (ai-context.ts) so docs can
* reference it directly via the `require.main === module` exec tail below; that
* copy is gitignored and refreshed on every analyze, so it cannot drift for long.
*/
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const NPX_REF = 'gitnexus@latest';
// Native packages whose postinstall must run under pnpm 10+ (blocked by default).
const PNPM_ALLOW_BUILD_BASE = ['@ladybugdb/core', 'gitnexus', 'tree-sitter'];
const PNPM_ALLOW_BUILD_EMBEDDINGS = ['onnxruntime-node'];
// Version-probe timeout, kept under Claude Code's 10s hook budget. PATH presence
// detection is now spawn-free (resolveOnPath scans PATH directly), so the only
// subprocesses left are the version probes: in a linked worktree the stale-index
// hook first runs `git rev-parse --git-common-dir` (~2s) and `git rev-parse HEAD`
// (~3s); the pnpm path then adds up to two 1s `--version` probes (npm, pnpm), so
// the worst case is ~7s — within budget. A healthy `--version` returns in well
// under a second, so the realistic cost is far lower.
const PROBE_TIMEOUT_MS = 1000;
/**
* Absolute path to `command` on PATH, or null — a pure-Node, spawn-free lookup
* that mirrors how a shell resolves a bare command name: each PATH dir × the
* platform's executable extensions (PATHEXT on Windows; the bare name + X_OK on
* POSIX). This replaces the former `where`/`which` subprocess (#1938 "Option A"):
* it is byte-for-byte identical on every OS, with no dependency on the probe
* binary being reachable (a sanitized PATH that drops System32 / `/usr/bin` no
* longer defeats detection), no shell-spawn surface (CVE-2024-27980), and no
* spawn timeout to tune. On Windows it matches PATHEXT extensions ONLY — exactly
* what `where`/cmd.exe resolve — so neither an un-spawnable `.ps1`-only shim (not
* in default PATHEXT) nor a bare extensionless file (which the shell cannot launch
* as `command`) is a false positive. `preferExecExt` returns a recognized
* `.cmd`/`.bat`/`.exe` shim ahead of an exotic PATHEXT hit (e.g. `.COM`) when both
* match, matching what a user would actually launch. Pure (platform/env injectable)
* so it is unit-testable without touching the host PATH.
*/
function resolveOnPath(
command,
preferExecExt = false,
{ platform = process.platform, env = process.env } = {},
) {
const pathValue = env.PATH || env.Path || env.path || '';
if (!pathValue) return null;
const isWin = platform === 'win32';
const exts = isWin
? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
.split(';')
.map((e) => e.trim())
.filter(Boolean)
.map((e) => (e.startsWith('.') ? e : `.${e}`))
: [''];
let weakHit = null;
// Split on the host's PATH delimiter. `platform` is injected only to choose the
// extension/exec-bit rules; the PATH string is always host-format, so it must
// split on the host delimiter (`path.delimiter`) — in production `platform` IS
// the host, so they coincide. (Deriving the delimiter from an injected platform
// would split a Windows drive-letter path `C:\…` at its colon under a POSIX
// injection.)
for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
for (const ext of exts) {
const candidate = path.join(dir, `${command}${ext}`);
try {
if (!fs.statSync(candidate).isFile()) continue;
if (!isWin) fs.accessSync(candidate, fs.constants.X_OK);
// Prefer a runnable .cmd/.bat/.exe shim; remember an exotic PATHEXT hit
// (e.g. .COM) only as a last resort if nothing better turns up.
if (isWin && preferExecExt && !/\.(cmd|bat|exe)$/i.test(ext)) {
weakHit = weakHit || candidate;
continue;
}
return candidate;
} catch {
/* not a runnable file here — try the next candidate */
}
}
}
return weakHit;
}
// One spawn of `<command> --version` → { major, minor } (each null when
// unreadable). Version injection happens at the resolver seam (getNpmMajorVersion
// / formatPnpmAllowBuildArgs), so this stays a pure real-process probe.
function probeVersion(command) {
try {
const output = execFileSync(command, ['--version'], {
encoding: 'utf-8',
timeout: PROBE_TIMEOUT_MS,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
// On Windows, npm/pnpm resolve to `.cmd` shims; execFileSync does no
// PATHEXT resolution and Node refuses to spawn `.cmd`/`.bat` without a
// shell (CVE-2024-27980), so a bare `<command> --version` ENOENTs and the
// probe would wrongly report a present tool as absent. A shell lets the OS
// resolve the shim. POSIX needs no shell (direct PATH lookup works).
shell: process.platform === 'win32',
});
// Find the first line that starts with a version token (`MAJOR.MINOR`,
// optional `v` prefix) rather than splitting the whole output — pnpm/npm
// under Corepack or with an update notice can print a banner line on stdout
// before the version (stderr is already dropped via the stdio config).
const versionLine = output
.split('\n')
.map((l) => l.trim())
.find((l) => /^v?\d+\.\d+/.test(l));
const match = versionLine ? versionLine.match(/^v?(\d+)\.(\d+)/) : null;
return {
major: match ? Number(match[1]) : null,
minor: match ? Number(match[2]) : null,
};
} catch {
return { major: null, minor: null };
}
}
// `deps` is the single injection seam: an explicitly provided key — including a
// `null` value, detected via `in` — is honored as-is so tests can simulate an
// absent tool without spawning; an absent key falls through to the real probe.
function getNpmMajorVersion(deps = {}) {
return 'npmMajor' in deps ? deps.npmMajor : probeVersion('npm').major;
}
/**
* `--allow-build` flags for the pre-`dlx` position. Emitted for pnpm >= 10.2
* (where the flag exists, and pnpm 10+ blocks build scripts by default). Omitted
* below 10.2: pnpm < 10 runs build scripts anyway, and pnpm 10.0/10.1 lack the
* flag (it would be rejected as an unknown option). `alwaysAllowBuild` forces the
* flags for committed documentation, which cannot probe the reader's pnpm.
*/
function formatPnpmAllowBuildArgs(options = {}, deps = {}) {
if (!options.alwaysAllowBuild) {
const { major, minor } =
'pnpmMajor' in deps
? { major: deps.pnpmMajor, minor: 'pnpmMinor' in deps ? deps.pnpmMinor : null }
: probeVersion('pnpm');
const lacksAllowBuild =
major !== null && (major < 10 || (major === 10 && minor !== null && minor < 2));
if (lacksAllowBuild) return [];
}
const pkgs = [...PNPM_ALLOW_BUILD_BASE];
if (options.embeddings) pkgs.push(...PNPM_ALLOW_BUILD_EMBEDDINGS);
return pkgs.map((p) => `--allow-build=${p}`);
}
/** Fixed install-free command for committed AGENTS.md / SKILL.md (pnpm >= 10.2). */
function formatDocumentationDlxCommand(gitnexusArgs, options = {}) {
const flags = formatPnpmAllowBuildArgs({ ...options, alwaysAllowBuild: true }).join(' ');
const prefix = flags ? `${flags} ` : '';
return `pnpm ${prefix}dlx ${NPX_REF} ${gitnexusArgs}`;
}
/**
* Resolve `gitnexus` | `pnpm` | `npx`. `GITNEXUS_INVOCATION` forces a mode
* (test/escape hatch). `probe` is injectable so the preference order can be
* unit-tested without spawning; it defaults to the real PATH probe. `deps` can
* inject `{ npmMajor, pnpmMajor }` for tests.
*/
function resolveInvocationMode(probe = resolveOnPath, deps = {}) {
const forced = process.env.GITNEXUS_INVOCATION?.trim().toLowerCase();
if (forced === 'gitnexus' || forced === 'pnpm' || forced === 'npx') {
return forced;
}
if (probe('gitnexus', true)) return 'gitnexus';
const npmMajor = getNpmMajorVersion(deps);
// pnpm presence: prefer an explicit `pnpmPresent` flag (set by
// formatAnalyzeCommand, which falls back to a PATH probe when the version is
// unreadable) so a present-but-unparseable pnpm — slow probe, Corepack
// banner — still selects pnpm instead of the npx crash path. Otherwise an
// injected version (a successful `pnpm --version` proves presence)
// short-circuits the `which pnpm` probe; failing both, fall back to PATH.
const hasPnpm =
'pnpmPresent' in deps
? deps.pnpmPresent
: 'pnpmMajor' in deps
? deps.pnpmMajor !== null
: Boolean(probe('pnpm'));
// npm 11+ npx install crash (#1939) — prefer pnpm dlx when available.
if (hasPnpm && npmMajor !== null && npmMajor >= 11) return 'pnpm';
// npm 10 and earlier: npx works; prefer it over pnpm dlx when npm is present.
if (npmMajor !== null && npmMajor < 11) return 'npx';
// npm absent or unreadable — use pnpm if present (with allow-build flags).
if (hasPnpm) return 'pnpm';
return 'npx';
}
function formatPnpmDlxCommand(gitnexusArgs, options = {}, deps = {}) {
const flags = formatPnpmAllowBuildArgs(options, deps).join(' ');
const prefix = flags ? `${flags} ` : '';
return `pnpm ${prefix}dlx ${NPX_REF} ${gitnexusArgs}`;
}
function formatAnalyzeCommand(options = {}, deps = {}) {
const suffix = options.embeddings ? ' --embeddings' : '';
// Keep the stale-index hook budget tight by querying each tool at most once.
// The memoized `probe` is a spawn-free PATH scan (resolveOnPath) shared with
// resolveInvocationMode, so `gitnexus` is scanned only once and no subprocess
// is spawned for presence. pnpm's *version* is still captured by a single
// `pnpm --version` (the allow-build gate needs the number), which also proves
// presence; the memoized scan only re-checks pnpm when that version is
// unreadable. Injected deps (tests) and forced/global modes skip the pnpm probe.
const cache = new Map();
const probe = (command, gitnexusWrapper) => {
const key = `${command}:${gitnexusWrapper ? 1 : 0}`;
if (!cache.has(key)) cache.set(key, resolveOnPath(command, gitnexusWrapper));
return cache.get(key);
};
let resolved = deps;
if (!('pnpmMajor' in deps)) {
const forced = process.env.GITNEXUS_INVOCATION?.trim().toLowerCase();
// pnpm is only consulted when no non-pnpm mode is already certain: forced
// gitnexus/npx never use pnpm, and a present global gitnexus wins outright.
const mightUsePnpm = forced === 'pnpm' || (forced !== 'gitnexus' && forced !== 'npx');
if (mightUsePnpm && (forced === 'pnpm' || !probe('gitnexus', true))) {
const { major, minor } = probeVersion('pnpm');
// Carry presence separately from version: when the version probe fails
// (timeout, Corepack banner) but pnpm is on PATH, still treat it as
// present so mode resolution picks pnpm over the npx crash path. The
// PATH probe is memoized and only runs when the version is unreadable.
const pnpmPresent = major !== null || Boolean(probe('pnpm'));
resolved = { ...deps, pnpmMajor: major, pnpmMinor: minor, pnpmPresent };
}
}
const mode = resolveInvocationMode(probe, resolved);
if (mode === 'gitnexus') return `gitnexus analyze${suffix}`;
if (mode === 'pnpm') return `${formatPnpmDlxCommand(`analyze${suffix}`, options, resolved)}`;
return `npx ${NPX_REF} analyze${suffix}`;
}
/**
* Resolve `mode` into a concrete { program, args } pair for a set of gitnexus
* subcommand arguments. Shared by the direct-exec entrypoint below; pure (no
* spawn) so it is unit-testable. `--embeddings` widens the pnpm allow-build set.
*/
function buildRunnerArgv(mode, gitnexusArgs, deps = {}) {
// Match both the space form (`--embeddings`) and the equals form
// (`--embeddings=5000`) Commander accepts, so the pnpm allow-build set still
// widens to onnxruntime-node when a user hand-types the equals form.
const embeddings = gitnexusArgs.some(
(a) => a === '--embeddings' || a.startsWith('--embeddings='),
);
if (mode === 'gitnexus') return { program: 'gitnexus', args: [...gitnexusArgs] };
if (mode === 'pnpm') {
return {
program: 'pnpm',
args: [...formatPnpmAllowBuildArgs({ embeddings }, deps), 'dlx', NPX_REF, ...gitnexusArgs],
};
}
return { program: 'npx', args: [NPX_REF, ...gitnexusArgs] };
}
module.exports = {
formatAnalyzeCommand,
formatDocumentationDlxCommand,
formatPnpmAllowBuildArgs,
formatPnpmDlxCommand,
resolveInvocationMode,
buildRunnerArgv,
resolveOnPath,
getNpmMajorVersion,
NPX_REF,
PNPM_ALLOW_BUILD_BASE,
};
// Direct-exec entrypoint (#1945): `node run.cjs <gitnexus args…>` resolves the
// best available runner (global `gitnexus` → `pnpm dlx` → `npx`) at call time and
// runs it, inheriting stdio and propagating the child's exit code. This lets the
// committed skills and generated AGENTS.md/CLAUDE.md reference ONE stable,
// CLI-neutral command without baking in a package-manager assumption. `gitnexus
// analyze` drops a copy of this file at `.gitnexus/run.cjs`. Skipped on require()
// (the CLI and tests reuse the exports above), so it runs only when invoked as a
// script.
if (require.main === module) {
const gitnexusArgs = process.argv.slice(2);
const { program, args } = buildRunnerArgv(resolveInvocationMode(), gitnexusArgs);
try {
execFileSync(program, args, {
stdio: 'inherit',
windowsHide: true,
// On Windows, `npx`/`pnpm`/`gitnexus` resolve to `.cmd`/`.ps1`/`.exe`
// shims (npm, Volta, Corepack, scoop). execFileSync does not do PATHEXT
// resolution and Node refuses to spawn `.cmd`/`.bat` without a shell
// (CVE-2024-27980), so a bare program name ENOENTs. A shell lets the OS
// resolve the shim; POSIX needs no shell (direct PATH lookup works).
shell: process.platform === 'win32',
});
} catch (err) {
// Make spawn failures (resolved program absent from PATH) self-explanatory
// instead of a silent exit 1, then propagate the runner's own exit code.
if (typeof err.status !== 'number') {
process.stderr.write(`gitnexus runner: could not launch \`${program}\` — ${err.message}\n`);
}
process.exit(typeof err.status === 'number' ? err.status : 1);
}
}
@@ -5,14 +5,16 @@ description: "Use when the user needs to run GitNexus CLI commands like analyze/
# GitNexus CLI Commands
All commands work via `npx` — no global install required.
Commands below use `node .gitnexus/run.cjs <command>` — the project-local runner `gitnexus analyze` drops next to the index. It auto-selects an available runner at call time (global `gitnexus`, else `pnpm dlx`, else `npx`), so no package-manager assumption and no global install is required.
> **Not analyzed yet, or `node .gitnexus/run.cjs` reports `Cannot find module`** (the gitignored runner is absent — e.g. a fresh clone or `git clean`)? (Re)generate it with `npx gitnexus analyze` from the project root. On **npm 11.x**, if `npx` crashes during install (`node.target is null`), install once with `npm i -g gitnexus` (then `gitnexus analyze`) or use `pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze`. See [#1939](https://github.com/abhigyanpatwari/GitNexus/issues/1939).
## Commands
### analyze — Build or refresh the index
```bash
npx gitnexus analyze
node .gitnexus/run.cjs analyze
```
Run from the project root. This parses all source files, builds the knowledge graph, writes it to `.gitnexus/`, and generates CLAUDE.md / AGENTS.md context files.
@@ -28,7 +30,7 @@ Run from the project root. This parses all source files, builds the knowledge gr
### status — Check index freshness
```bash
npx gitnexus status
node .gitnexus/run.cjs status
```
Shows whether the current repo has a GitNexus index, when it was last updated, and symbol/relationship counts. Use this to check if re-indexing is needed.
@@ -36,7 +38,7 @@ Shows whether the current repo has a GitNexus index, when it was last updated, a
### clean — Delete the index
```bash
npx gitnexus clean
node .gitnexus/run.cjs clean
```
Deletes the `.gitnexus/` directory and unregisters the repo from the global registry. Use before re-indexing if the index is corrupt or after removing GitNexus from a project.
@@ -49,7 +51,7 @@ Deletes the `.gitnexus/` directory and unregisters the repo from the global regi
### wiki — Generate documentation from the graph
```bash
npx gitnexus wiki
node .gitnexus/run.cjs wiki
```
Generates repository documentation from the knowledge graph using an LLM. Requires an API key (saved to `~/.gitnexus/config.json` on first use).
@@ -68,7 +70,7 @@ Generates repository documentation from the knowledge graph using an LLM. Requir
### list — Show all indexed repos
```bash
npx gitnexus list
node .gitnexus/run.cjs list
```
Lists all repositories registered in `~/.gitnexus/registry.json`. The MCP `list_repos` tool provides the same information.
@@ -16,23 +16,23 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@@ -40,7 +40,7 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
| Symptom | GitNexus Approach |
| -------------------- | ---------------------------------------------------------- |
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@@ -48,24 +48,24 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
@@ -75,11 +75,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
@@ -18,20 +18,20 @@ description: "Use when the user asks how code works, wants to understand archite
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
> If step 2 says "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@@ -47,18 +47,18 @@ description: "Use when the user asks how code works, wants to understand archite
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@@ -68,10 +68,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
@@ -15,7 +15,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
2. **Match your task to a skill below** and **read that skill file**
3. **Follow the skill's workflow and checklist**
> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first.
> If step 1 warns the index is stale, run `node .gitnexus/run.cjs analyze` in the terminal first.
## Skills
@@ -17,22 +17,22 @@ description: "Use when the user wants to know what will break if they change som
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@@ -55,10 +55,10 @@ description: "Use when the user wants to know what will break if they change som
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@@ -73,10 +73,10 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@@ -86,7 +86,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
@@ -18,24 +18,24 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal before reviewing.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal before reviewing.
## Checklist
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:
@@ -16,78 +16,78 @@ description: "Use when the user wants to rename, extract, split, move, or restru
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
@@ -98,24 +98,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
| ------------------- | ----------------------------------------- |
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```
+1 -1
View File
@@ -40,7 +40,7 @@ If you already have a `.cursor/hooks.json`, merge the `hooks.postToolUse` array
### Verify
1. Index the project: `npx gitnexus analyze`
1. Index the project: `npx gitnexus analyze` (on npm 11.x, `npx` can crash during install — use `pnpm --allow-build=@ladybugdb/core --allow-build=gitnexus --allow-build=tree-sitter dlx gitnexus@latest analyze` instead; see [#1939](https://github.com/abhigyanpatwari/GitNexus/issues/1939))
2. Reload the Cursor window so it picks up the new hook config.
3. Ask the agent something that triggers `Read` / `Grep` / `Shell rg`. You should see a `[GitNexus]` block appended to the tool result.
4. Diagnose silent no-ops by setting `GITNEXUS_DEBUG=1` in your shell environment — the hook will write Cursor's raw event payload to stderr so you can verify field names.
@@ -15,23 +15,23 @@ description: Trace bugs through call chains using knowledge graph
## Workflow
```
1. gitnexus_query({query: "<error or symptom>"}) → Find related execution flows
2. gitnexus_context({name: "<suspect>"}) → See callers/callees/processes
1. query({query: "<error or symptom>"}) → Find related execution flows
2. context({name: "<suspect>"}) → See callers/callees/processes
3. READ gitnexus://repo/{name}/process/{name} → Trace execution flow
4. gitnexus_cypher({query: "MATCH path..."}) → Custom traces if needed
4. cypher({query: "MATCH path..."}) → Custom traces if needed
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] Understand the symptom (error message, unexpected behavior)
- [ ] gitnexus_query for error text or related code
- [ ] query for error text or related code
- [ ] Identify the suspect function from returned processes
- [ ] gitnexus_context to see callers and callees
- [ ] context to see callers and callees
- [ ] Trace execution flow via process resource if applicable
- [ ] gitnexus_cypher for custom call chain traces if needed
- [ ] cypher for custom call chain traces if needed
- [ ] Read source files to confirm root cause
```
@@ -39,7 +39,7 @@ description: Trace bugs through call chains using knowledge graph
| Symptom | GitNexus Approach |
|---------|-------------------|
| Error message | `gitnexus_query` for error text → `context` on throw sites |
| Error message | `query` for error text → `context` on throw sites |
| Wrong return value | `context` on the function → trace callees for data flow |
| Intermittent failure | `context` → look for external calls, async deps |
| Performance issue | `context` → find symbols with many callers (hot paths) |
@@ -47,22 +47,22 @@ description: Trace bugs through call chains using knowledge graph
## Tools
**gitnexus_query** — find code related to error:
**query** — find code related to error:
```
gitnexus_query({query: "payment validation error"})
query({query: "payment validation error"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError, PaymentException
```
**gitnexus_context** — full context for a suspect:
**context** — full context for a suspect:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates (external API!)
→ Processes: CheckoutFlow (step 3/7)
```
**gitnexus_cypher** — custom call chain traces:
**cypher** — custom call chain traces:
```cypher
MATCH path = (a)-[:CodeRelation {type: 'CALLS'}*1..2]->(b:Function {name: "validatePayment"})
RETURN [n IN nodes(path) | n.name] AS chain
@@ -71,11 +71,11 @@ RETURN [n IN nodes(path) | n.name] AS chain
## Example: "Payment endpoint returns 500 intermittently"
```
1. gitnexus_query({query: "payment error handling"})
1. query({query: "payment error handling"})
→ Processes: CheckoutFlow, ErrorHandling
→ Symbols: validatePayment, handlePaymentError
2. gitnexus_context({name: "validatePayment"})
2. context({name: "validatePayment"})
→ Outgoing calls: verifyCard, fetchRates (external API!)
3. READ gitnexus://repo/my-app/process/CheckoutFlow
@@ -17,20 +17,20 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
```
1. READ gitnexus://repos → Discover indexed repos
2. READ gitnexus://repo/{name}/context → Codebase overview, check staleness
3. gitnexus_query({query: "<what you want to understand>"}) → Find related execution flows
4. gitnexus_context({name: "<symbol>"}) → Deep dive on specific symbol
3. query({query: "<what you want to understand>"}) → Find related execution flows
4. context({name: "<symbol>"}) → Deep dive on specific symbol
5. READ gitnexus://repo/{name}/process/{name} → Trace full execution flow
```
> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal.
> If step 2 says "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] READ gitnexus://repo/{name}/context
- [ ] gitnexus_query for the concept you want to understand
- [ ] query for the concept you want to understand
- [ ] Review returned processes (execution flows)
- [ ] gitnexus_context on key symbols for callers/callees
- [ ] context on key symbols for callers/callees
- [ ] READ process resource for full execution traces
- [ ] Read source files for implementation details
```
@@ -46,16 +46,16 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
## Tools
**gitnexus_query** — find execution flows related to a concept:
**query** — find execution flows related to a concept:
```
gitnexus_query({query: "payment processing"})
query({query: "payment processing"})
→ Processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Symbols grouped by flow with file locations
```
**gitnexus_context** — 360-degree view of a symbol:
**context** — 360-degree view of a symbol:
```
gitnexus_context({name: "validateUser"})
context({name: "validateUser"})
→ Incoming calls: loginHandler, apiMiddleware
→ Outgoing calls: checkToken, getUserById
→ Processes: LoginFlow (step 2/5), TokenRefresh (step 1/3)
@@ -65,10 +65,10 @@ gitnexus_context({name: "validateUser"})
```
1. READ gitnexus://repo/my-app/context → 918 symbols, 45 processes
2. gitnexus_query({query: "payment processing"})
2. query({query: "payment processing"})
→ CheckoutFlow: processPayment → validateCard → chargeStripe
→ RefundFlow: initiateRefund → calculateRefund → processRefund
3. gitnexus_context({name: "processPayment"})
3. context({name: "processPayment"})
→ Incoming: checkoutHandler, webhookHandler
→ Outgoing: validateCard, chargeStripe, saveTransaction
4. Read src/payments/processor.ts for implementation details
@@ -16,22 +16,22 @@ description: Analyze blast radius before making code changes
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → What depends on this
1. impact({target: "X", direction: "upstream"}) → What depends on this
2. READ gitnexus://repo/{name}/processes → Check affected execution flows
3. gitnexus_detect_changes() → Map current git changes to affected flows
3. detect_changes() → Map current git changes to affected flows
4. Assess risk and report to user
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklist
```
- [ ] gitnexus_impact({target, direction: "upstream"}) to find dependents
- [ ] impact({target, direction: "upstream"}) to find dependents
- [ ] Review d=1 items first (these WILL BREAK)
- [ ] Check high-confidence (>0.8) dependencies
- [ ] READ processes to check affected execution flows
- [ ] gitnexus_detect_changes() for pre-commit check
- [ ] detect_changes() for pre-commit check
- [ ] Assess risk level and report to user
```
@@ -54,9 +54,9 @@ description: Analyze blast radius before making code changes
## Tools
**gitnexus_impact** — the primary tool for symbol blast radius:
**impact** — the primary tool for symbol blast radius:
```
gitnexus_impact({
impact({
target: "validateUser",
direction: "upstream",
minConfidence: 0.8,
@@ -71,9 +71,9 @@ gitnexus_impact({
- authRouter (src/routes/auth.ts:22) [CALLS, 95%]
```
**gitnexus_detect_changes** — git-diff based impact analysis:
**detect_changes** — git-diff based impact analysis:
```
gitnexus_detect_changes({scope: "staged"})
detect_changes({scope: "staged"})
→ Changed: 5 symbols in 3 files
→ Affected: LoginFlow, TokenRefresh, APIMiddlewarePipeline
@@ -83,7 +83,7 @@ gitnexus_detect_changes({scope: "staged"})
## Example: "What breaks if I change validateUser?"
```
1. gitnexus_impact({target: "validateUser", direction: "upstream"})
1. impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware (WILL BREAK)
→ d=2: authRouter, sessionManager (LIKELY AFFECTED)
@@ -18,24 +18,24 @@ description: "Use when the user wants to review a pull request, understand what
```
1. gh pr diff <number> → Get the raw diff
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
2. detect_changes({scope: "compare", base_ref: "main"}) → Map diff to affected flows
3. For each changed symbol:
gitnexus_impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. gitnexus_context({name: "<key symbol>"}) → Understand callers/callees
impact({target: "<symbol>", direction: "upstream"}) → Blast radius per change
4. context({name: "<key symbol>"}) → Understand callers/callees
5. READ gitnexus://repo/{name}/processes → Check affected execution flows
6. Summarize findings with risk assessment
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal before reviewing.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal before reviewing.
## Checklist
```
- [ ] Fetch PR diff (gh pr diff or git diff base...head)
- [ ] gitnexus_detect_changes to map changes to affected execution flows
- [ ] gitnexus_impact on each non-trivial changed symbol
- [ ] detect_changes to map changes to affected execution flows
- [ ] impact on each non-trivial changed symbol
- [ ] Review d=1 items (WILL BREAK) — are callers updated?
- [ ] gitnexus_context on key changed symbols to understand full picture
- [ ] context on key changed symbols to understand full picture
- [ ] Check if affected processes have test coverage
- [ ] Assess overall risk level
- [ ] Write review summary with findings
@@ -63,20 +63,20 @@ description: "Use when the user wants to review a pull request, understand what
## Tools
**gitnexus_detect_changes** — map PR diff to affected execution flows:
**detect_changes** — map PR diff to affected execution flows:
```
gitnexus_detect_changes({scope: "compare", base_ref: "main"})
detect_changes({scope: "compare", base_ref: "main"})
→ Changed: 8 symbols in 4 files
→ Affected processes: CheckoutFlow, RefundFlow, WebhookHandler
→ Risk: MEDIUM
```
**gitnexus_impact** — blast radius per changed symbol:
**impact** — blast radius per changed symbol:
```
gitnexus_impact({target: "validatePayment", direction: "upstream"})
impact({target: "validatePayment", direction: "upstream"})
→ d=1 (WILL BREAK):
- processCheckout (src/checkout.ts:42) [CALLS, 100%]
@@ -86,20 +86,20 @@ gitnexus_impact({target: "validatePayment", direction: "upstream"})
- checkoutRouter (src/routes/checkout.ts:22) [CALLS, 95%]
```
**gitnexus_impact with tests** — check test coverage:
**impact with tests** — check test coverage:
```
gitnexus_impact({target: "validatePayment", direction: "upstream", includeTests: true})
impact({target: "validatePayment", direction: "upstream", includeTests: true})
→ Tests that cover this symbol:
- validatePayment.test.ts [direct]
- checkout.integration.test.ts [via processCheckout]
```
**gitnexus_context** — understand a changed symbol's role:
**context** — understand a changed symbol's role:
```
gitnexus_context({name: "validatePayment"})
context({name: "validatePayment"})
→ Incoming calls: processCheckout, webhookHandler
→ Outgoing calls: verifyCard, fetchRates
@@ -112,20 +112,20 @@ gitnexus_context({name: "validatePayment"})
1. gh pr diff 42 > /tmp/pr42.diff
→ 4 files changed: payments.ts, checkout.ts, types.ts, utils.ts
2. gitnexus_detect_changes({scope: "compare", base_ref: "main"})
2. detect_changes({scope: "compare", base_ref: "main"})
→ Changed symbols: validatePayment, PaymentInput, formatAmount
→ Affected processes: CheckoutFlow, RefundFlow
→ Risk: MEDIUM
3. gitnexus_impact({target: "validatePayment", direction: "upstream"})
3. impact({target: "validatePayment", direction: "upstream"})
→ d=1: processCheckout, webhookHandler (WILL BREAK)
→ webhookHandler is NOT in the PR diff — potential breakage!
4. gitnexus_impact({target: "PaymentInput", direction: "upstream"})
4. impact({target: "PaymentInput", direction: "upstream"})
→ d=1: validatePayment (in PR), createPayment (NOT in PR)
→ createPayment uses the old PaymentInput shape — breaking change!
5. gitnexus_context({name: "formatAmount"})
5. context({name: "formatAmount"})
→ Called by 12 functions — but change is backwards-compatible (added optional param)
6. Review summary:
@@ -15,72 +15,72 @@ description: Plan safe refactors using blast radius and dependency mapping
## Workflow
```
1. gitnexus_impact({target: "X", direction: "upstream"}) → Map all dependents
2. gitnexus_query({query: "X"}) → Find execution flows involving X
3. gitnexus_context({name: "X"}) → See all incoming/outgoing refs
1. impact({target: "X", direction: "upstream"}) → Map all dependents
2. query({query: "X"}) → Find execution flows involving X
3. context({name: "X"}) → See all incoming/outgoing refs
4. Plan update order: interfaces → implementations → callers → tests
```
> If "Index is stale" → run `npx gitnexus analyze` in terminal.
> If "Index is stale" → run `node .gitnexus/run.cjs analyze` in terminal.
## Checklists
### Rename Symbol
```
- [ ] gitnexus_rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] rename({symbol_name: "oldName", new_name: "newName", dry_run: true}) — preview all edits
- [ ] Review graph edits (high confidence) and ast_search edits (review carefully)
- [ ] If satisfied: gitnexus_rename({..., dry_run: false}) — apply edits
- [ ] gitnexus_detect_changes() — verify only expected files changed
- [ ] If satisfied: rename({..., dry_run: false}) — apply edits
- [ ] detect_changes() — verify only expected files changed
- [ ] Run tests for affected processes
```
### Extract Module
```
- [ ] gitnexus_context({name: target}) — see all incoming/outgoing refs
- [ ] gitnexus_impact({target, direction: "upstream"}) — find all external callers
- [ ] context({name: target}) — see all incoming/outgoing refs
- [ ] impact({target, direction: "upstream"}) — find all external callers
- [ ] Define new module interface
- [ ] Extract code, update imports
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
### Split Function/Service
```
- [ ] gitnexus_context({name: target}) — understand all callees
- [ ] context({name: target}) — understand all callees
- [ ] Group callees by responsibility
- [ ] gitnexus_impact({target, direction: "upstream"}) — map callers to update
- [ ] impact({target, direction: "upstream"}) — map callers to update
- [ ] Create new functions/services
- [ ] Update callers
- [ ] gitnexus_detect_changes() — verify affected scope
- [ ] detect_changes() — verify affected scope
- [ ] Run tests for affected processes
```
## Tools
**gitnexus_rename** — automated multi-file rename:
**rename** — automated multi-file rename:
```
gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits across 8 files
→ 10 graph edits (high confidence), 2 ast_search edits (review)
→ Changes: [{file_path, edits: [{line, old_text, new_text, confidence}]}]
```
**gitnexus_impact** — map all dependents first:
**impact** — map all dependents first:
```
gitnexus_impact({target: "validateUser", direction: "upstream"})
impact({target: "validateUser", direction: "upstream"})
→ d=1: loginHandler, apiMiddleware, testUtils
→ Affected Processes: LoginFlow, TokenRefresh
```
**gitnexus_detect_changes** — verify your changes after refactoring:
**detect_changes** — verify your changes after refactoring:
```
gitnexus_detect_changes({scope: "all"})
detect_changes({scope: "all"})
→ Changed: 8 files, 12 symbols
→ Affected processes: LoginFlow, TokenRefresh
→ Risk: MEDIUM
```
**gitnexus_cypher** — custom reference queries:
**cypher** — custom reference queries:
```cypher
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "validateUser"})
RETURN caller.name, caller.filePath ORDER BY caller.filePath
@@ -90,24 +90,24 @@ RETURN caller.name, caller.filePath ORDER BY caller.filePath
| Risk Factor | Mitigation |
|-------------|------------|
| Many callers (>5) | Use gitnexus_rename for automated updates |
| Many callers (>5) | Use rename for automated updates |
| Cross-area refs | Use detect_changes after to verify scope |
| String/dynamic refs | gitnexus_query to find them |
| String/dynamic refs | query to find them |
| External/public API | Version and deprecate properly |
## Example: Rename `validateUser` to `authenticateUser`
```
1. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
1. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: true})
→ 12 edits: 10 graph (safe), 2 ast_search (review)
→ Files: validator.ts, login.ts, middleware.ts, config.json...
2. Review ast_search edits (config.json: dynamic reference!)
3. gitnexus_rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
3. rename({symbol_name: "validateUser", new_name: "authenticateUser", dry_run: false})
→ Applied 12 edits across 8 files
4. gitnexus_detect_changes({scope: "all"})
4. detect_changes({scope: "all"})
→ Affected: LoginFlow, TokenRefresh
→ Risk: MEDIUM — run tests for these flows
```
+17 -1
View File
@@ -115,7 +115,23 @@ export type RelationshipType =
| 'HANDLES_TOOL'
| 'ENTRY_POINT_OF'
| 'WRAPS'
| 'QUERIES';
| 'QUERIES'
/** Vue component event system: a handler function in a parent component is
* bound to an event emitted by a child component (`@event="handlerFn"`).
* Source = handler Function/Method node in the parent.
* Target = the child component's File node.
* `reason` encodes the event name: `vue-event: @<eventName>`.
* Complements `EMITS_EVENT`; together they enable Cypher queries that
* trace which handlers receive which component's emitted events. */
| 'BINDS_EVENT_HANDLER'
/** Vue component event system: a component calls `emit('eventName', ...)`
* or `this.$emit('eventName', ...)`, advertising that it can emit that event.
* Source = the component's own File node (self-referential annotation).
* Target = the same File node.
* `reason` encodes the event name: `vue-emit: <eventName>`.
* Complements `BINDS_EVENT_HANDLER`; a Cypher query joining on the
* component File node reveals all (emitter, handler) pairs. */
| 'EMITS_EVENT';
export interface GraphNode {
id: string;
+8 -11
View File
@@ -116,6 +116,8 @@ export type {
FieldRegistry,
FieldLookupOptions,
} from './scope-resolution/registries/field-registry.js';
export { buildMacroRegistry } from './scope-resolution/registries/macro-registry.js';
export type { MacroRegistry } from './scope-resolution/registries/macro-registry.js';
export { lookupCore } from './scope-resolution/registries/lookup-core.js';
export type { CoreLookupParams } from './scope-resolution/registries/lookup-core.js';
export { lookupQualified } from './scope-resolution/registries/lookup-qualified.js';
@@ -127,7 +129,12 @@ export {
CONFIDENCE_EPSILON,
} from './scope-resolution/registries/tie-breaks.js';
export type { TieBreakKey } from './scope-resolution/registries/tie-breaks.js';
export { CLASS_KINDS, METHOD_KINDS, FIELD_KINDS } from './scope-resolution/registries/context.js';
export {
CLASS_KINDS,
METHOD_KINDS,
FIELD_KINDS,
MACRO_KINDS,
} from './scope-resolution/registries/context.js';
export type {
RegistryContext,
RegistryProviders,
@@ -176,13 +183,3 @@ export {
stripGitSuffix,
} from './integrations/understand-quickly.js';
export type { UqDispatchPayload } from './integrations/understand-quickly.js';
// Shadow-mode diff + aggregation (RFC §6.3; Ring 2 SHARED #918)
export { diffResolutions } from './scope-resolution/shadow/diff.js';
export type {
ShadowAgreement,
ShadowCallsite,
ShadowDiff,
} from './scope-resolution/shadow/diff.js';
export { aggregateDiffs } from './scope-resolution/shadow/aggregate.js';
export type { LanguageParityRow, ShadowParityReport } from './scope-resolution/shadow/aggregate.js';
+1
View File
@@ -10,6 +10,7 @@ export type PipelinePhase =
| 'imports'
| 'calls'
| 'heritage'
| 'scopeResolution'
| 'communities'
| 'processes'
| 'enriching'
@@ -8,8 +8,7 @@
*
* Part of RFC #909 Ring 2 SHARED — #913.
*
* Consumed by: #915 (SCC finalize link pass), #923 (shadow harness when
* resolving callsite file → enclosing module).
* Consumed by: #915 (SCC finalize link pass).
*/
import type { ScopeId } from './types.js';
@@ -74,4 +74,28 @@ export interface ParsedFile {
*/
readonly localDefs: readonly SymbolDefinition[];
readonly referenceSites: readonly ReferenceSite[];
/**
* Opaque, language-private serialization of capture-time side-channel
* state that a provider's `emitScopeCaptures` populates into module-level
* maps as a SIDE EFFECT (not onto the scopes/defs of this `ParsedFile`).
*
* Such state is computed inside the parse worker (where `emitScopeCaptures`
* runs) and would otherwise be lost across the worker→main MessageChannel
* and the disk store, because scope-resolution reuses the serialized
* `ParsedFile` and SKIPS re-extraction on the main thread (#1983 — the
* whole point is to avoid a main-thread tree-sitter re-parse). Carrying the
* data here lets the main thread repopulate those maps WITHOUT re-parsing.
*
* Shared / ingestion code treats this as opaque (`unknown`) per AGENTS.md
* (no language names in shared code). The producing language fills it via
* the `LanguageProvider.collectCaptureSideChannel` hook (worker side) and
* consumes it via the `ScopeResolver.applyCaptureSideChannel` hook
* (main-thread resolution side). It MUST be plain JSON-serializable data
* (objects / arrays / primitives) so it round-trips through the disk-backed
* `parsedfile-store` (JSON.stringify + interning reviver).
*
* Optional: providers whose `emitScopeCaptures` is pure (no module-level
* side effects — the contract default) leave this undefined.
*/
readonly captureSideChannel?: unknown;
}
@@ -37,7 +37,12 @@ export type ReferenceKind =
| 'write'
| 'type-reference'
| 'inherits'
| 'import-use';
| 'import-use'
// A macro invocation (`log!(...)` / `vec![...]`). Resolved against
// `Macro`-labeled definitions ONLY (see `MacroRegistry`) so a macro
// never aliases a same-named free function — macros and functions are
// disjoint namespaces. Emitted as a `USES` edge, not `CALLS`.
| 'macro';
/**
* How a call site binds its target. Informs `Registry.lookup` Step 2
@@ -54,6 +59,18 @@ export type CallForm = 'free' | 'member' | 'constructor' | 'index';
export interface ReferenceSite {
/** The name being referenced (e.g., `'save'`, `'User'`, `'count'`). */
readonly name: string;
/**
* Optional raw, qualified form of the referenced name when the source wrote
* a qualified path (e.g. a C++ base `struct D : Other::Inner` yields
* `'Other::Inner'`). `name` keeps the simple tail (`'Inner'`) for the existing
* scope-chain contract; resolution normalizes this via `normalizeQualifiedName`
* and resolves it against the full-path `QualifiedNameIndex` BEFORE the
* simple-tail walk, so a same-tail nested base resolves to the correct
* sibling instead of the first-inserted one (issue #1982). Populated only by
* per-language captures that emit `@reference.qualified-name`; absent
* otherwise, in which case resolution is unchanged.
*/
readonly rawQualifiedName?: string;
/** Source-text range of this reference. */
readonly atRange: Range;
/**
@@ -162,3 +162,10 @@ export const FIELD_KINDS: readonly NodeLabel[] = Object.freeze([
'Const',
'Static',
]);
// Macros occupy a namespace disjoint from functions/methods: a `log!`
// invocation must resolve ONLY to a `macro_rules! log` definition, never
// to a same-named `fn log`. `MACRO_KINDS` is therefore a singleton
// (`['Macro']`) and is NOT merged into METHOD_KINDS — keeping the two
// keyspaces separate is what prevents the cross-namespace false-edge.
export const MACRO_KINDS: readonly NodeLabel[] = Object.freeze(['Macro']);
@@ -57,8 +57,7 @@ export interface RawSignals {
*
* Emission order mirrors the `EvidenceWeights` layout: where-found →
* type-binding → corroborators → arity → degraded. Stable order makes
* the per-signal contributions easy to reason about in tests and in the
* shadow-mode parity dashboard.
* the per-signal contributions easy to reason about in tests.
*/
export function composeEvidence(signals: RawSignals): readonly ResolutionEvidence[] {
const out: ResolutionEvidence[] = [];
@@ -141,7 +140,7 @@ export function composeEvidence(signals: RawSignals): readonly ResolutionEvidenc
/**
* Sum evidence weights and clamp to `[0, 1]`. Separate from `composeEvidence`
* so tests and the parity dashboard can inspect the raw evidence list.
* so tests can inspect the raw evidence list.
*/
export function confidenceFromEvidence(evidence: readonly ResolutionEvidence[]): number {
let sum = 0;
@@ -0,0 +1,43 @@
/**
* `MacroRegistry` — scope-aware lookup for macro definitions
* (`macro_rules!` in Rust; `#define` in C/C++) referenced from a macro
* invocation site.
*
* Thin wrapper over `lookupCore`, specialized for the macro namespace:
*
* - `acceptedKinds` = `MACRO_KINDS` (`['Macro']` only). Crucially this
* does NOT include `Function`/`Method`, so a `log!(…)` invocation can
* never resolve to a same-named free function `fn log` — macros and
* functions are disjoint namespaces (the false-`CALLS`-edge class the
* #1934 review flagged).
* - `useReceiverTypeBinding` is **false** — a macro invocation has no
* receiver; resolution is name-through-the-lexical-chain + the global
* qualified fallback, exactly like `ClassRegistry`.
* - Arity is not applied — macros are variadic by nature.
*/
import type { Resolution, ScopeId } from '../types.js';
import { lookupCore, type CoreLookupParams } from './lookup-core.js';
import { MACRO_KINDS, type RegistryContext } from './context.js';
export interface MacroRegistry {
/**
* Look up a macro definition by simple or scoped name anchored at
* `scope`. Returns a confidence-ranked `Resolution[]`; consume `[0]`
* for the best answer.
*/
lookup(name: string, scope: ScopeId): readonly Resolution[];
}
export function buildMacroRegistry(ctx: RegistryContext): MacroRegistry {
const params: CoreLookupParams = {
acceptedKinds: MACRO_KINDS,
useReceiverTypeBinding: false,
ownerScopedContributor: null,
};
return {
lookup(name: string, scope: ScopeId) {
return lookupCore(name, scope, params, ctx);
},
};
}
@@ -1,188 +0,0 @@
/**
* Shadow-mode aggregation — per-language parity %, per-evidence-kind
* breakdown of divergences. Consumed by the parity dashboard (RING2-PKG-5).
*
* Pure functions; no I/O. The harness persists per-run JSON; the dashboard
* reads `.gitnexus/shadow-parity/latest.json` and renders.
*
* Related types — `ShadowAgreement`, `ShadowCallsite`, `ShadowDiff` — are
* defined alongside `diffResolutions` in `./diff.ts` and re-exported
* through the top-level `gitnexus-shared` barrel. Consumers import all
* three from `gitnexus-shared`, not from this module.
*
* Part of RFC #909 Ring 2 SHARED — #918.
*/
import type { SupportedLanguages } from '../../languages.js';
import type { ResolutionEvidence } from '../types.js';
import type { ShadowAgreement, ShadowDiff } from './diff.js';
// ─── Aggregated report shape ────────────────────────────────────────────────
export interface LanguageParityRow {
readonly language: SupportedLanguages;
readonly totalCalls: number;
readonly bothAgree: number;
readonly onlyLegacy: number;
readonly onlyNew: number;
readonly bothDisagree: number;
readonly bothEmpty: number;
/**
* Fraction in [0, 1]. Numerator = `bothAgree`; denominator = "calls where
* at least one side resolved" = `totalCalls - bothEmpty`.
*
* When the denominator is 0 (all calls for this language were
* `both-empty`), returns 0. Callers rendering the dashboard should treat
* a 0 parity alongside `totalCalls === bothEmpty` as "no signal" rather
* than "total disagreement".
*/
readonly parity: number;
/**
* Divergence signals broken down by `ResolutionEvidence.kind`. Sourced
* from `ShadowDiff.evidenceDelta` on non-agreeing rows only — `both-agree`
* and `both-empty` do not contribute.
*/
readonly evidenceBreakdown: ReadonlyMap<ResolutionEvidence['kind'], number>;
}
export interface ShadowParityReport {
readonly generatedAt: string; // ISO 8601
readonly perLanguage: readonly LanguageParityRow[];
readonly overall: Omit<LanguageParityRow, 'language' | 'evidenceBreakdown'>;
}
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Aggregate a stream of `ShadowDiff` records into a `ShadowParityReport`,
* bucketed by language. Pure function.
*
* - `perLanguage` rows are sorted alphabetically by `SupportedLanguages`
* value for stable JSON output (the dashboard reads
* `.gitnexus/shadow-parity/latest.json` and diffing snapshots is useful).
* - `overall` is the column-wise sum across languages.
* - `generatedAt` is injected via the `now` parameter so tests stay
* deterministic; production callers let it default to `new Date()`.
*/
export function aggregateDiffs(
diffs: readonly { readonly language: SupportedLanguages; readonly diff: ShadowDiff }[],
now: Date = new Date(),
): ShadowParityReport {
const perLanguageMap = new Map<SupportedLanguages, MutableCounts>();
for (const { language, diff } of diffs) {
let counts = perLanguageMap.get(language);
if (!counts) {
counts = makeEmptyCounts();
perLanguageMap.set(language, counts);
}
tallyDiff(counts, diff);
}
const perLanguage: LanguageParityRow[] = Array.from(perLanguageMap.entries())
.map(([language, counts]) => buildRow(language, counts))
.sort((a, b) => a.language.localeCompare(b.language));
const overall = buildOverallRow(perLanguage);
return {
generatedAt: now.toISOString(),
perLanguage,
overall,
};
}
// ─── Internal helpers ───────────────────────────────────────────────────────
interface MutableCounts {
totalCalls: number;
bothAgree: number;
onlyLegacy: number;
onlyNew: number;
bothDisagree: number;
bothEmpty: number;
evidenceBreakdown: Map<ResolutionEvidence['kind'], number>;
}
function makeEmptyCounts(): MutableCounts {
return {
totalCalls: 0,
bothAgree: 0,
onlyLegacy: 0,
onlyNew: 0,
bothDisagree: 0,
bothEmpty: 0,
evidenceBreakdown: new Map(),
};
}
function tallyDiff(counts: MutableCounts, diff: ShadowDiff): void {
counts.totalCalls += 1;
incrementAgreement(counts, diff.agreement);
if (diff.agreement === 'both-agree' || diff.agreement === 'both-empty') return;
for (const ev of diff.evidenceDelta) {
counts.evidenceBreakdown.set(ev.kind, (counts.evidenceBreakdown.get(ev.kind) ?? 0) + 1);
}
}
function incrementAgreement(counts: MutableCounts, agreement: ShadowAgreement): void {
switch (agreement) {
case 'both-agree':
counts.bothAgree += 1;
return;
case 'only-legacy':
counts.onlyLegacy += 1;
return;
case 'only-new':
counts.onlyNew += 1;
return;
case 'both-disagree':
counts.bothDisagree += 1;
return;
case 'both-empty':
counts.bothEmpty += 1;
return;
}
}
function buildRow(language: SupportedLanguages, counts: MutableCounts): LanguageParityRow {
const resolved = counts.totalCalls - counts.bothEmpty;
const parity = resolved > 0 ? counts.bothAgree / resolved : 0;
return {
language,
totalCalls: counts.totalCalls,
bothAgree: counts.bothAgree,
onlyLegacy: counts.onlyLegacy,
onlyNew: counts.onlyNew,
bothDisagree: counts.bothDisagree,
bothEmpty: counts.bothEmpty,
parity,
// Freeze via `new Map` on a sorted-kind copy so downstream consumers
// can't mutate the aggregator's internal state.
evidenceBreakdown: new Map(
Array.from(counts.evidenceBreakdown.entries()).sort(([a], [b]) => a.localeCompare(b)),
),
};
}
function buildOverallRow(
perLanguage: readonly LanguageParityRow[],
): Omit<LanguageParityRow, 'language' | 'evidenceBreakdown'> {
let totalCalls = 0;
let bothAgree = 0;
let onlyLegacy = 0;
let onlyNew = 0;
let bothDisagree = 0;
let bothEmpty = 0;
for (const row of perLanguage) {
totalCalls += row.totalCalls;
bothAgree += row.bothAgree;
onlyLegacy += row.onlyLegacy;
onlyNew += row.onlyNew;
bothDisagree += row.bothDisagree;
bothEmpty += row.bothEmpty;
}
const resolved = totalCalls - bothEmpty;
const parity = resolved > 0 ? bothAgree / resolved : 0;
return { totalCalls, bothAgree, onlyLegacy, onlyNew, bothDisagree, bothEmpty, parity };
}
@@ -1,126 +0,0 @@
/**
* Shadow-mode diff logic — RFC §6.3.
*
* Pure comparison logic for shadow mode. Takes two `Resolution[]` (legacy
* DAG result + new scope-based registry result) and produces a structured
* diff record for the parity dashboard.
*
* Consumed by the Ring 2 PKG shadow harness (#923), which dual-runs each
* call through legacy + new paths, diffs results, and persists per-run JSON
* for the parity dashboard.
*
* Part of RFC #909 Ring 2 SHARED — #918.
*/
import type { Resolution, ResolutionEvidence } from '../types.js';
// ─── Diff record shape ──────────────────────────────────────────────────────
export type ShadowAgreement =
| 'both-agree' // top match identical (same DefId)
| 'only-legacy' // legacy resolved; new did not
| 'only-new' // new resolved; legacy did not
| 'both-disagree' // both resolved, but to different targets
| 'both-empty'; // both returned empty
export interface ShadowDiff {
readonly callsite: ShadowCallsite;
readonly legacy: Resolution | null;
readonly newResult: Resolution | null;
readonly agreement: ShadowAgreement;
/**
* Symmetric difference of the two top resolutions' `evidence` arrays,
* keyed on `ResolutionEvidence.kind`.
*
* - For `'both-agree'` and `'both-empty'` agreements, always empty.
* - For `'both-disagree'`, contains evidence kinds present on exactly one
* side (not in both).
* - For `'only-legacy'`, contains all of legacy's top evidence.
* - For `'only-new'`, contains all of new's top evidence.
*/
readonly evidenceDelta: readonly ResolutionEvidence[];
}
export interface ShadowCallsite {
readonly filePath: string;
readonly line: number;
readonly col: number;
readonly calledName: string;
}
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Compare two `Resolution[]` arrays (top matches at `[0]`) and produce a
* `ShadowDiff`. Pure function.
*
* Agreement rules:
* - both arrays empty → `'both-empty'`, `evidenceDelta: []`
* - legacy empty, new non-empty → `'only-new'`, `evidenceDelta` = new's top evidence
* - legacy non-empty, new empty → `'only-legacy'`, `evidenceDelta` = legacy's top evidence
* - both non-empty, same top `def.nodeId` → `'both-agree'`, `evidenceDelta: []`
* - both non-empty, different top `def.nodeId` → `'both-disagree'`,
* `evidenceDelta` = symmetric difference by `ResolutionEvidence.kind`
* (first occurrence of a kind-only-on-legacy then kind-only-on-new; order
* preserved from input arrays)
*
* Evidence-delta rationale: callers aggregating divergences want to know
* which signal kinds explain a disagreement. Keying on `kind` (not full
* equality over `weight`/`note`) avoids spurious deltas when the same
* signal fires with slightly different calibration weights on each side.
*/
export function diffResolutions(
callsite: ShadowCallsite,
legacy: readonly Resolution[],
newResult: readonly Resolution[],
): ShadowDiff {
const legacyTop: Resolution | null = legacy.length > 0 ? legacy[0] : null;
const newTop: Resolution | null = newResult.length > 0 ? newResult[0] : null;
const agreement: ShadowAgreement = (() => {
if (legacyTop === null && newTop === null) return 'both-empty';
if (legacyTop === null) return 'only-new';
if (newTop === null) return 'only-legacy';
return legacyTop.def.nodeId === newTop.def.nodeId ? 'both-agree' : 'both-disagree';
})();
const evidenceDelta = computeEvidenceDelta(legacyTop, newTop, agreement);
return {
callsite,
legacy: legacyTop,
newResult: newTop,
agreement,
evidenceDelta,
};
}
// ─── Internal helpers ───────────────────────────────────────────────────────
/**
* Symmetric difference of two evidence arrays, keyed on
* `ResolutionEvidence.kind`. Preserves input order: legacy-only signals
* first (in legacy's original order), then new-only signals (in new's order).
*
* For `'both-agree'` / `'both-empty'` the delta is empty by contract. For
* `'only-legacy'` / `'only-new'` one side's evidence is the delta (nothing to
* subtract against).
*/
function computeEvidenceDelta(
legacy: Resolution | null,
newResult: Resolution | null,
agreement: ShadowAgreement,
): readonly ResolutionEvidence[] {
if (agreement === 'both-agree' || agreement === 'both-empty') return [];
if (agreement === 'only-legacy') return legacy!.evidence;
if (agreement === 'only-new') return newResult!.evidence;
// both-disagree: symmetric difference keyed on `kind`
const legacyKinds = new Set(legacy!.evidence.map((e) => e.kind));
const newKinds = new Set(newResult!.evidence.map((e) => e.kind));
const onlyInLegacy = legacy!.evidence.filter((e) => !newKinds.has(e.kind));
const onlyInNew = newResult!.evidence.filter((e) => !legacyKinds.has(e.kind));
return [...onlyInLegacy, ...onlyInNew];
}
@@ -53,6 +53,19 @@ export interface SymbolDefinition {
* `ScopeResolver.constraintCompatibility` hook during overload narrowing.
* Absent for symbols that have no constraints (the common case). */
templateConstraints?: unknown;
/** True when the producing language marked this callable as explicit.
* Currently used by C++ overload ranking to exclude explicit constructors
* from implicit user-defined conversion candidates. */
isExplicit?: boolean;
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
ownerId?: string;
/** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`)
* tagged during the C++ resolution phase. Lets the graph bridge retry a
* namespace-prefixed node-lookup key and lets the qualified-base resolver
* break same-tail cross-namespace inheritance ties. A deliberate sidecar,
* separate from `qualifiedName`: it does NOT participate in graph node
* identity (node keys derive from filePath/type/qualifiedName) and leaves the
* qualifiedName-keyed resolution index untouched. Absent for the common case
* (non-namespace-nested defs and all non-C++ languages). */
namespacePrefix?: string;
}
@@ -267,8 +267,11 @@ export interface ScopeLookup {
/** Call-site description passed to `arityCompatibility`. */
export interface Callsite {
/** Number of arguments at the call site. */
readonly arity: number;
/** Number of arguments at the call site, if available. */
readonly arity?: number;
/** Inferred argument types at the call site, one per argument.
* An empty string entry means the type was not inferred. */
readonly argumentTypes?: readonly string[];
}
// ─── §2.4 ImportEdge ────────────────────────────────────────────────────────
@@ -436,7 +439,7 @@ export interface Reference {
readonly toDef: DefId;
/** Location of the reference in source. */
readonly atRange: Range;
readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use';
readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use' | 'macro';
readonly confidence: number;
readonly evidence: readonly ResolutionEvidence[];
}
+117
View File
@@ -0,0 +1,117 @@
import { test, expect } from '@playwright/test';
/**
* E2E tests for graph layout mode switching (Sequential / Radial layouts).
*
* Requires:
* - gitnexus serve running on localhost:4747 with at least one indexed repo
* - gitnexus-web dev server running on localhost:5173
*
* Skipped when servers aren't available (CI without services, etc.).
* Set E2E=1 to force-run even without the availability check.
*/
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:4747';
const FRONTEND_URL = process.env.FRONTEND_URL ?? 'http://localhost:5173';
test.beforeAll(async () => {
if (process.env.E2E) return;
try {
const [backendRes, frontendRes] = await Promise.allSettled([
fetch(`${BACKEND_URL}/api/repos`),
fetch(FRONTEND_URL),
]);
if (
backendRes.status === 'rejected' ||
(backendRes.status === 'fulfilled' && !backendRes.value.ok)
) {
test.skip(true, 'gitnexus serve not available on :4747');
return;
}
if (
frontendRes.status === 'rejected' ||
(frontendRes.status === 'fulfilled' && !frontendRes.value.ok)
) {
test.skip(true, 'Vite dev server not available on :5173');
return;
}
if (backendRes.status === 'fulfilled') {
const repos = await backendRes.value.json();
if (!repos.length) {
test.skip(true, 'No indexed repos — run gitnexus analyze first');
return;
}
}
} catch {
test.skip(true, 'servers not available');
}
});
async function waitForGraphLoaded(page: import('@playwright/test').Page) {
await page.goto(`${FRONTEND_URL}?lng=en`);
// The app starts on the landing/onboarding screen. Pick the first repo card
// (preferring a known repo name) and click it to load the graph.
const landingCards = page.locator('[data-testid="landing-repo-card"]');
const preferredCard = landingCards.filter({ hasText: /GitNexus|local-integration/ }).first();
try {
await landingCards.first().waitFor({ state: 'visible', timeout: 15_000 });
const card = (await preferredCard.count()) > 0 ? preferredCard : landingCards.first();
await card.click();
} catch {
// Landing screen may not appear (e.g. when ?server auto-connects)
}
// Wait until the status bar confirms the graph is ready.
const statusBar = page.getByRole('contentinfo');
await expect(statusBar.getByText('Ready', { exact: true })).toBeVisible({ timeout: 45_000 });
await expect(statusBar).toContainText(/nodes/, { timeout: 20_000 });
// Finally confirm the sigma canvas is present.
await page.waitForSelector('.sigma-container', { timeout: 10_000 });
}
test.describe('Graph Layout Modes', () => {
test.beforeEach(async ({ page }) => {
await waitForGraphLoaded(page);
});
test('should switch between force, sequential, and radial layouts', async ({ page }) => {
const forceTab = page.locator('button:has-text("Force Graph")');
const sequentialTab = page.locator('button:has-text("Sequential Layout")');
const radialTab = page.locator('button:has-text("Radial Layout")');
// Force Graph is the default active tab
await expect(forceTab).toHaveClass(/bg-accent/);
await expect(sequentialTab).not.toHaveClass(/bg-accent/);
// Switch to Sequential Layout
await sequentialTab.click();
await expect(sequentialTab).toHaveClass(/bg-accent/, { timeout: 5_000 });
await expect(forceTab).not.toHaveClass(/bg-accent/);
// All three layout tabs should be present in the tab bar
await expect(radialTab).toBeVisible();
// Switch back to Force Graph
await forceTab.click();
await expect(forceTab).toHaveClass(/bg-accent/, { timeout: 5_000 });
await expect(sequentialTab).not.toHaveClass(/bg-accent/);
});
test('should interact with nodes in sequential layout', async ({ page }) => {
await page.locator('button:has-text("Sequential Layout")').click();
// Click the first file-tree item in the sidebar (more reliable than a
// blind canvas click, which may land on empty space). The FileTreePanel
// renders node names as <span class="truncate font-mono text-xs">.
// Clicking any of them calls setSelectedNode, which shows the selection
// bar with the "Clear" button — the same mechanism used in
// server-connect.spec.ts's "Turn Off All Highlights" test.
const firstTreeItem = page.locator('span.truncate.font-mono').first();
await firstTreeItem.waitFor({ state: 'visible', timeout: 10_000 });
await firstTreeItem.click();
await expect(page.locator('text=Clear')).toBeVisible({ timeout: 5_000 });
});
});
+130 -137
View File
@@ -11,14 +11,14 @@
"@langchain/anthropic": "^1.3.29",
"@langchain/core": "^1.1.44",
"@langchain/google-genai": "^2.1.30",
"@langchain/langgraph": "^1.2.9",
"@langchain/langgraph": "^1.3.2",
"@langchain/ollama": "^1.2.6",
"@langchain/openai": "^1.4.5",
"@sigma/edge-curve": "^3.1.0",
"@tailwindcss/vite": "^4.3.0",
"axios": "^1.16.0",
"axios": "^1.16.1",
"d3": "^7.9.0",
"dompurify": "^3.4.3",
"dompurify": "^3.4.7",
"gitnexus-shared": "file:../gitnexus-shared",
"graphology": "^0.26.0",
"graphology-indices": "^0.17.0",
@@ -26,11 +26,11 @@
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
"graphology-utils": "^2.3.0",
"i18next": "^26.2.0",
"i18next": "^26.3.0",
"i18next-browser-languagedetector": "^8.2.1",
"langchain": "^1.3.5",
"langchain": "^1.4.2",
"lru-cache": "^11.2.4",
"lucide-react": "^1.14.0",
"lucide-react": "^1.16.0",
"mermaid": "^11.15.0",
"mnemonist": "^0.39.0",
"pandemonium": "^2.4.0",
@@ -48,16 +48,16 @@
},
"devDependencies": {
"@babel/types": "^7.29.0",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.2.0",
"@types/node": "^25.6.0",
"@types/node": "^25.9.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/node": "^5.8.2",
"@vercel/node": "^5.8.8",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^4.1.5",
"jsdom": "^29.1.1",
@@ -1357,16 +1357,13 @@
}
},
"node_modules/@langchain/core": {
"version": "1.1.45",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.45.tgz",
"integrity": "sha512-Y/wvuglLTMKJahkl4QD9dBIdF/z/CxZJWdTfHJF/q2jtlJtoFf6Mb5JpGxZfsi3mBY6NSG941FSLTcqhCKrhBA==",
"version": "1.1.48",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.48.tgz",
"integrity": "sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==",
"license": "MIT",
"dependencies": {
"@cfworker/json-schema": "^4.0.2",
"@standard-schema/spec": "^1.1.0",
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
"js-tiktoken": "^1.0.12",
"langsmith": ">=0.5.0 <1.0.0",
"mustache": "^4.2.0",
@@ -1393,13 +1390,14 @@
}
},
"node_modules/@langchain/langgraph": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.2.9.tgz",
"integrity": "sha512-3c7BtGycHC2v9p6w/Hv8L7kEl1YnZYOQTDJtmAp3knk6JOedO7d2bYP3y0SRyhv5orUEGf/KGvx8ZsB/ideP7g==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.3.2.tgz",
"integrity": "sha512-SL7Ktsr681R7da+1b2MVOWEbaCoFJOXEJPTGOjg4JIG4C7quWbTYC8DzxhcCxte6D/8cGp0rYDBnbKLXEpNqlA==",
"license": "MIT",
"dependencies": {
"@langchain/langgraph-checkpoint": "^1.0.1",
"@langchain/langgraph-sdk": "~1.8.9",
"@langchain/langgraph-checkpoint": "^1.0.2",
"@langchain/langgraph-sdk": "~1.9.4",
"@langchain/protocol": "^0.0.15",
"@standard-schema/spec": "1.1.0",
"uuid": "^10.0.0"
},
@@ -1407,7 +1405,7 @@
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": "^1.1.40",
"@langchain/core": "^1.1.44",
"zod": "^3.25.32 || ^4.2.0",
"zod-to-json-schema": "^3.x"
},
@@ -1418,9 +1416,9 @@
}
},
"node_modules/@langchain/langgraph-checkpoint": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.1.tgz",
"integrity": "sha512-HM0cJLRpIsSlWBQ/xuDC67l52SqZ62Bh2Y61DX+Xorqwoh5e1KxYvfCD7GnSTbWWhjBOutvnR0vPhu4orFkZfw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.2.tgz",
"integrity": "sha512-F4E5Tr0nt8FGghgdscJtHw+ABzChOHeI80R7Y1pjIHdiJom6c2ieo76vL+FWiny80JmoGqhrVAEIWrw0cXKPxg==",
"license": "MIT",
"dependencies": {
"uuid": "^10.0.0"
@@ -1429,7 +1427,7 @@
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": "^1.0.1"
"@langchain/core": "^1.1.44"
}
},
"node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": {
@@ -1446,27 +1444,25 @@
}
},
"node_modules/@langchain/langgraph-sdk": {
"version": "1.8.10",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.8.10.tgz",
"integrity": "sha512-wrB3rkRw5KAmsqezwvKP3midT4qJrV6Hj9XJMYo+cbvXC4HYpSAmyY/VriSyeTFRbLG/OP/pY2Yz+9Z54nSaXQ==",
"version": "1.9.9",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.9.tgz",
"integrity": "sha512-aiWHbmqxWj5sAMwFsaB3eSGQvKpMbUKTlt9zbAC0T7IiFqDYUWi9gJUGsTdvJutAfB3P/NzC4s8ETUtUQEUlYg==",
"license": "MIT",
"dependencies": {
"@langchain/protocol": "^0.0.15",
"@types/json-schema": "^7.0.15",
"p-queue": "^9.0.1",
"p-retry": "^7.1.1",
"uuid": "^13.0.0"
},
"peerDependencies": {
"@langchain/core": "^1.1.16",
"@langchain/core": "^1.1.44",
"react": "^18 || ^19",
"react-dom": "^18 || ^19",
"svelte": "^4.0.0 || ^5.0.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"@langchain/core": {
"optional": true
},
"react": {
"optional": true
},
@@ -1488,9 +1484,9 @@
"license": "MIT"
},
"node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz",
"integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==",
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz",
"integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.4",
@@ -1516,9 +1512,9 @@
}
},
"node_modules/@langchain/langgraph-sdk/node_modules/uuid": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.1.tgz",
"integrity": "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw==",
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -1587,6 +1583,12 @@
"@langchain/core": "^1.1.42"
}
},
"node_modules/@langchain/protocol": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.15.tgz",
"integrity": "sha512-MllvbpMjqHevUm+v94M422mH7XKN+wGCvJRBVROTWBotEDOATYB4Ktk2UheYP859y9o2LlhtPek5t1T9eyfAbQ==",
"license": "MIT"
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz",
@@ -1684,13 +1686,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
@@ -1960,9 +1962,9 @@
"license": "MIT"
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2425,9 +2427,9 @@
"license": "MIT"
},
"node_modules/@ts-morph/common/node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2854,13 +2856,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"version": "25.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@types/prismjs": {
@@ -2928,9 +2930,9 @@
}
},
"node_modules/@vercel/build-utils": {
"version": "13.25.0",
"resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.25.0.tgz",
"integrity": "sha512-p2wqxi2I95T+g/+uP+Dc/uq2PApW7F9RbE/Vvwp28JY8SoCGHNUs2pxigttgaNDNF6IlUEMOTz+eJvsXToV/1w==",
"version": "13.26.4",
"resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.26.4.tgz",
"integrity": "sha512-0g3ZxtZUJZbt4y0Vu4pkHtu1UN58FbVF9cqGT8T6jHp0EHdLGFj5TVCiME8ALeK4tjPImSmxnKZvvB5yb2hqEw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2954,9 +2956,9 @@
"license": "Apache-2.0"
},
"node_modules/@vercel/nft": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.5.0.tgz",
"integrity": "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.10.0.tgz",
"integrity": "sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2981,9 +2983,9 @@
}
},
"node_modules/@vercel/node": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.2.tgz",
"integrity": "sha512-Wt6KBr0LoIhUuzeH7E8S+1HRlS6oSA+98FJH/59wY2tYoxsHXbx5uiNkSBqBsM0nIwlJWR3B86tm9q9Xi22XdQ==",
"version": "5.8.8",
"resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.8.tgz",
"integrity": "sha512-+uRT9evnGWUE6klrJJED4fCvlSxNShbIc/UY4FeUzt2sdcy5a5b1IoYlo94RJd7tAY9Jg2lR2cVfGfsnWH81ZA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2991,10 +2993,10 @@
"@edge-runtime/primitives": "4.1.0",
"@edge-runtime/vm": "3.2.0",
"@types/node": "20.11.0",
"@vercel/build-utils": "13.25.0",
"@vercel/build-utils": "13.26.4",
"@vercel/error-utils": "2.1.0",
"@vercel/nft": "1.5.0",
"@vercel/static-config": "3.3.0",
"@vercel/nft": "1.10.0",
"@vercel/static-config": "3.4.0",
"async-listen": "3.0.0",
"cjs-module-lexer": "1.2.3",
"edge-runtime": "2.5.9",
@@ -3055,9 +3057,9 @@
}
},
"node_modules/@vercel/static-config": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.3.0.tgz",
"integrity": "sha512-GpS3tPwUeDJCkrKbMNtS2XLRFgfxTlN7YNUL+Bo23+fGolrDw6Oq79R3yvxTYgqRaJMGSEqC7iMw6mj6I5loxg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.4.0.tgz",
"integrity": "sha512-wCq90CMUB//ggnFh77NQO1xaLFsS4LigQIqKrH6ohnr9Br/KI1FhlErx62WfCOuueWaW+LVsbLOqNXIUjK8t6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -3326,7 +3328,9 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -3414,16 +3418,42 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/axios/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -3567,18 +3597,6 @@
"node": ">= 0.4"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001764",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
@@ -4365,15 +4383,6 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
@@ -4452,9 +4461,9 @@
"peer": true
},
"node_modules/dompurify": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.3.tgz",
"integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==",
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
"integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -4998,22 +5007,6 @@
"node": ">= 6"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -5303,9 +5296,9 @@
}
},
"node_modules/i18next": {
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.2.0.tgz",
"integrity": "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==",
"version": "26.3.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.0.tgz",
"integrity": "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==",
"funding": [
{
"type": "individual",
@@ -5444,9 +5437,9 @@
}
},
"node_modules/is-network-error": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz",
"integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
"license": "MIT",
"engines": {
"node": ">=16"
@@ -5759,12 +5752,12 @@
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
},
"node_modules/langchain": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/langchain/-/langchain-1.3.5.tgz",
"integrity": "sha512-QSB8TEo6G1tWupgNt1Osm8ylLLoOMq1lLw5NeijnIwRPI5BqdBjUn4/U8usbjEJJcQnbXSK2qTKgTx7zvblnBw==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/langchain/-/langchain-1.4.2.tgz",
"integrity": "sha512-SLGipy0r4nqQD0aiUOBYLMeGFfB/QiYnMndfZ8sGN89vXDCIXbYqcE7G/4QDDX3nZsM7/emQpoScmlxEX6sDnQ==",
"license": "MIT",
"dependencies": {
"@langchain/langgraph": "^1.2.9",
"@langchain/langgraph": "^1.3.2",
"@langchain/langgraph-checkpoint": "^1.0.1",
"langsmith": ">=0.5.0 <1.0.0",
"zod": "^3.25.76 || ^4"
@@ -5773,7 +5766,7 @@
"node": ">=20"
},
"peerDependencies": {
"@langchain/core": "^1.1.42"
"@langchain/core": "^1.1.48"
}
},
"node_modules/langsmith": {
@@ -6111,9 +6104,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz",
"integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
"integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -7572,13 +7565,13 @@
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
@@ -7591,9 +7584,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -8319,9 +8312,9 @@
}
},
"node_modules/tar": {
"version": "7.5.15",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
"integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -8567,9 +8560,9 @@
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"devOptional": true,
"license": "MIT"
},
+9 -9
View File
@@ -21,14 +21,14 @@
"@langchain/anthropic": "^1.3.29",
"@langchain/core": "^1.1.44",
"@langchain/google-genai": "^2.1.30",
"@langchain/langgraph": "^1.2.9",
"@langchain/langgraph": "^1.3.2",
"@langchain/ollama": "^1.2.6",
"@langchain/openai": "^1.4.5",
"@sigma/edge-curve": "^3.1.0",
"@tailwindcss/vite": "^4.3.0",
"axios": "^1.16.0",
"axios": "^1.16.1",
"d3": "^7.9.0",
"dompurify": "^3.4.3",
"dompurify": "^3.4.7",
"gitnexus-shared": "file:../gitnexus-shared",
"graphology": "^0.26.0",
"graphology-indices": "^0.17.0",
@@ -36,11 +36,11 @@
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
"graphology-utils": "^2.3.0",
"i18next": "^26.2.0",
"i18next": "^26.3.0",
"i18next-browser-languagedetector": "^8.2.1",
"langchain": "^1.3.5",
"langchain": "^1.4.2",
"lru-cache": "^11.2.4",
"lucide-react": "^1.14.0",
"lucide-react": "^1.16.0",
"mermaid": "^11.15.0",
"mnemonist": "^0.39.0",
"pandemonium": "^2.4.0",
@@ -58,16 +58,16 @@
},
"devDependencies": {
"@babel/types": "^7.29.0",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/dompurify": "^3.2.0",
"@types/node": "^25.6.0",
"@types/node": "^25.9.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/node": "^5.8.2",
"@vercel/node": "^5.8.8",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^4.1.5",
"jsdom": "^29.1.1",
@@ -201,7 +201,10 @@ const getNodeTypeIcon = (label: NodeLabel) => {
case 'Import':
return FileCode;
case 'Variable':
case 'Property':
return Variable;
case 'Const':
return Target;
default:
return Variable;
}
+90 -17
View File
@@ -9,11 +9,16 @@ import {
Pause,
Lightbulb,
LightbulbOff,
Network,
GitBranch,
Target,
} from '@/lib/lucide-icons';
import { useSigma } from '../hooks/useSigma';
import { useAppState } from '../hooks/useAppState';
import {
knowledgeGraphToGraphology,
knowledgeGraphToTreeGraphology,
knowledgeGraphToCirclesGraphology,
filterGraphByDepth,
SigmaNodeAttributes,
SigmaEdgeAttributes,
@@ -48,6 +53,8 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
clearAICitationHighlights,
clearBlastRadius,
animatedNodes,
graphViewMode,
setGraphViewMode,
} = useAppState();
const [hoveredNodeName, setHoveredNodeName] = useState<string | null>(null);
@@ -149,8 +156,22 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
blastRadiusNodeIds: effectiveBlastRadiusNodeIds,
animatedNodes: effectiveAnimatedNodes,
visibleEdgeTypes,
layoutMode: graphViewMode,
});
const handleViewModeChange = useCallback(
(mode: 'force' | 'tree' | 'circles') => {
if (mode === graphViewMode) return;
setSelectedNode(null);
setSigmaSelectedNode(null);
setHoveredNodeName(null);
setGraphViewMode(mode);
// Reset zoom when switching views
resetZoom();
},
[graphViewMode, resetZoom, setGraphViewMode, setSelectedNode, setSigmaSelectedNode],
);
// Expose focusNode to parent via ref
useImperativeHandle(
ref,
@@ -174,25 +195,30 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
useEffect(() => {
if (!graph) return;
// Build communityMemberships map from MEMBER_OF relationships
// MEMBER_OF edges: nodeId -> communityId (stored as targetId)
const communityMemberships = new Map<string, number>();
graph.relationships.forEach((rel) => {
if (rel.type === 'MEMBER_OF') {
// Find the community node to get its index
const communityNode = nodeById.get(rel.targetId);
if (communityNode && communityNode.label === 'Community') {
// Extract community index from id (e.g., "comm_5" -> 5)
const numericPart = rel.targetId.replace('comm_', '');
const communityIdx = /^\d+$/.test(numericPart) ? parseInt(numericPart, 10) : 0;
communityMemberships.set(rel.sourceId, communityIdx);
let sigmaGraph: Graph<SigmaNodeAttributes, SigmaEdgeAttributes>;
if (graphViewMode === 'tree') {
sigmaGraph = knowledgeGraphToTreeGraphology(graph);
} else if (graphViewMode === 'circles') {
sigmaGraph = knowledgeGraphToCirclesGraphology(graph);
} else {
// Build community memberships map from MEMBER_OF relationships
const communityMemberships = new Map<string, number>();
graph.relationships.forEach((rel) => {
if (rel.type === 'MEMBER_OF') {
const communityNode = nodeById.get(rel.targetId);
if (communityNode && communityNode.label === 'Community') {
const numericPart = rel.targetId.replace('comm_', '');
const communityIdx = /^\d+$/.test(numericPart) ? parseInt(numericPart, 10) : 0;
communityMemberships.set(rel.sourceId, communityIdx);
}
}
}
});
});
sigmaGraph = knowledgeGraphToGraphology(graph, communityMemberships);
}
const sigmaGraph = knowledgeGraphToGraphology(graph, communityMemberships);
setSigmaGraph(sigmaGraph);
}, [graph, nodeById, setSigmaGraph]);
}, [graph, nodeById, setSigmaGraph, graphViewMode]);
// Update node visibility when filters change
useEffect(() => {
@@ -205,7 +231,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
filterGraphByDepth(sigmaGraph, appSelectedNode?.id || null, depthFilter, visibleLabels);
sigma.refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps -- sigmaRef identity never changes
}, [visibleLabels, depthFilter, appSelectedNode]);
}, [graph, graphViewMode, visibleLabels, depthFilter, appSelectedNode]);
// Sync app selected node with sigma
useEffect(() => {
@@ -245,6 +271,53 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
/>
</div>
{/* View Mode Tabs */}
<div
role="tablist"
aria-label={t('canvas.viewModes.label')}
className="absolute top-4 left-1/2 z-20 flex -translate-x-1/2 gap-1 rounded-lg border border-border-subtle bg-elevated/90 p-1 backdrop-blur-sm"
>
<button
role="tab"
aria-selected={graphViewMode === 'force'}
onClick={() => handleViewModeChange('force')}
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all ${
graphViewMode === 'force'
? 'bg-accent text-white'
: 'text-text-secondary hover:bg-hover hover:text-text-primary'
}`}
>
<Network className="h-3.5 w-3.5" />
{t('canvas.viewModes.force')}
</button>
<button
role="tab"
aria-selected={graphViewMode === 'tree'}
onClick={() => handleViewModeChange('tree')}
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all ${
graphViewMode === 'tree'
? 'bg-accent text-white'
: 'text-text-secondary hover:bg-hover hover:text-text-primary'
}`}
>
<GitBranch className="h-3.5 w-3.5" />
{t('canvas.viewModes.tree')}
</button>
<button
role="tab"
aria-selected={graphViewMode === 'circles'}
onClick={() => handleViewModeChange('circles')}
className={`flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all ${
graphViewMode === 'circles'
? 'bg-accent text-white'
: 'text-text-secondary hover:bg-hover hover:text-text-primary'
}`}
>
<Target className="h-3.5 w-3.5" />
{t('canvas.viewModes.circles')}
</button>
</div>
{/* Sigma container */}
<div
ref={containerRef}
@@ -13,6 +13,7 @@ import {
Check,
Loader2,
AlertCircle,
Square,
} from '@/lib/lucide-icons';
import type { ToolCallInfo } from '../core/llm/types';
import type { TFunction } from 'i18next';
@@ -77,6 +78,13 @@ const getStatusDisplay = (status: ToolCallInfo['status']) => {
bgColor: 'bg-rose-500/10',
borderColor: 'border-rose-500/30',
};
case 'stopped':
return {
icon: <Square className="h-3.5 w-3.5 fill-current" />,
color: 'text-amber-300',
bgColor: 'bg-amber-500/10',
borderColor: 'border-amber-500/30',
};
default:
return {
icon: <Sparkles className="h-3.5 w-3.5" />,
+3 -1
View File
@@ -2,7 +2,9 @@
export const ERROR_RESET_DELAY_MS = 3000;
export const BACKEND_URL_DEBOUNCE_MS = 500;
export const DEFAULT_BACKEND_URL = 'http://localhost:4747';
export const DEFAULT_BACKEND_URL =
(typeof window !== 'undefined' && window.__GITNEXUS_CONFIG__?.backendUrl) ||
'http://localhost:4747';
export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434';
export const DEFAULT_OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
+96 -47
View File
@@ -65,66 +65,90 @@ export const BASE_SYSTEM_PROMPT = `You are Nexus, a Code Analysis Agent with acc
## ⚠️ MANDATORY: GROUNDING
Every factual claim MUST include a citation.
- File refs: [[src/auth.ts:45-60]] (line range with hyphen)
- File refs: [[src/auth.ts:45-60]] (repo-relative path, line range with hyphen)
- Symbol refs: [[Function:validateUser]] or [[Class:AuthService]]
- Do NOT wrap citations in backticks or code blocks — keep them as plain text
- NO citation = NO claim. Say "I didn't find evidence" instead of guessing.
## ⚠️ MANDATORY: VALIDATION
Every output MUST be validated.
- Use cypher to validate the results and confirm completeness of context before final output.
- NO validation = NO claim. Say "I didn't find evidence" instead of guessing.
- Do not blindly trust readme or single source of truth. Always validate and cross-reference. Never be lazy.
## 🧠 CORE PROTOCOL (Iterative Loop)
You are an investigator, not a one-shot query engine. For each question:
1. **Plan** — Briefly state what you are looking for and why.
2. **Execute** — Run tools to gather evidence.
3. **Analyze & pivot** — Did the output fully answer the question?
- Yes → proceed to grounding.
- Revealed new files/functions → loop back and investigate them immediately.
- Tool failed → fix the input and retry. Never stop after one error.
4. **Trace** — Use cypher, explore, or impact to follow graph connections.
5. **Read** — Use read to verify logic. Do not guess behavior from names alone.
6. **Validate** — Cross-check findings with cypher before final output. README/docs are summaries, not proof.
7. **Ground** — Cite every finding with [[path:START-END]] or [[Type:Name]].
## 🧠 CORE PROTOCOL
You are an investigator. For each question:
1. **Search** → Use cypher, search or grep to find relevant code
2. **Read** → Use read to see the actual source
3. **Trace** → Use cypher to follow connections in the graph
4. **Cite** → Ground every finding with [[file:line]] or [[Type:Name]]
5. **Validate** → Use cypher to validate the results and confirm completeness of context before final output. ( MUST DO )
Before EVERY tool call, briefly state what you are doing and why. Keep narration to one line per step.
## 🛠️ TOOLS
- **\`search\`** — Hybrid search. Results grouped by process with cluster context.
- **\`cypher\`** — Cypher queries against the graph. Use \`{{QUERY_VECTOR}}\` for vector search.
- **\`grep\`** — Regex search. Best for exact strings, TODOs, error codes.
- **\`read\`** — Read file content. Always use after search/grep to see full code.
- **\`explore\`** — Deep dive on a symbol, cluster, or process. Shows membership, participation, connections.
## BE DIRECT
- No pleasantries. No "Great question!" or "I'd be happy to help."
- Don't repeat advice already given in this conversation.
- Match response length to query complexity.
- Don't pad with generic "let me know if you need more" — users will ask.
## 🛠️ TOOLS (exact names — use these only)
- **\`search\`** — Hybrid keyword + semantic search. Results grouped by process with cluster context. Start here for discovery.
- **\`cypher\`** — Cypher queries against the graph. Use \`{{QUERY_VECTOR}}\` placeholder for vector search.
- **\`grep\`** — Regex search across files. Best for exact strings, TODOs, error codes.
- **\`read\`** — Read file content. Always use after search/grep to see full source.
- **\`explore\`** — Deep dive on a symbol, cluster, or process.
- **\`overview\`** — Codebase map showing all clusters and processes.
- **\`impact\`** — Impact analysis. Shows affected processes, clusters, and risk level.
**Tool strategy:**
- Discovery → \`search\` or \`overview\`
- Structure → \`cypher\`, \`explore\`, or \`impact\`
- Verification → \`read\` (required before concluding)
- Exact patterns → \`grep\`
## 📊 GRAPH SCHEMA
Nodes: File, Folder, Function, Class, Interface, Method, Community, Process
Relations: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
Typed node labels: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
Single relation table: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
## 📐 GRAPH SEMANTICS (Important!)
**Edge Types:**
- \`CALLS\`: Method invocation OR constructor injection. If A receives B as parameter and uses it, A→B is CALLS. This is intentional simplification.
- \`IMPORTS\`: File-level import/include statement.
- \`EXTENDS/IMPLEMENTS\`: Class inheritance.
**Process Nodes:**
- Process labels use format: "EntryPoint → Terminal" (e.g., "onCreate → showToast")
- These are heuristic names from tracing execution flow, NOT application-defined names
- Entry points are detected via export status, naming patterns, and framework conventions
✅ \`MATCH (f:Function) RETURN f.name LIMIT 10\`
✅ \`MATCH (a)-[r:CodeRelation {type: 'CALLS'}]->(b:Function) RETURN a.name, b.name\`
❌ \`MATCH ()-[:CALLS]->()\` — WRONG, no such relationship label
Cypher examples:
- \`MATCH (f:Function) RETURN f.name LIMIT 10\`
- \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\`
- Find callers: \`MATCH (caller:Function)-[:CodeRelation {type: 'CALLS'}]->(fn:Function {name: 'validate'}) RETURN caller.name, caller.filePath\`
- File imports: \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\`
- Semantic search: include \`{{QUERY_VECTOR}}\` in cypher and provide a \`query\` parameter
## 📝CRITICAL RULES
- **impact output is trusted.** Do NOT re-validate with cypher. Optionally run the suggested grep commands for dynamic patterns.
## 📐 GRAPH SEMANTICS
- \`CALLS\`: Method invocation or constructor injection (intentional simplification).
- \`IMPORTS\`: File-level import/include.
- \`EXTENDS/IMPLEMENTS\`: Class inheritance.
- Process labels use format "EntryPoint → Terminal" (heuristic, not app-defined names).
## 🎯 VISUAL GROUNDING (not a tool)
The user sees a knowledge graph alongside this chat. Citations automatically highlight nodes in the graph UI.
- Include [[path:START-END]] and [[Type:Name]] refs as you discover relevant code — the UI highlights them for the user.
- Prefer 2-6 high-signal references over large dumps.
- There is NO \`highlight_in_graph\` tool. Ground with citations; the UI handles visualization.
## 📝 CRITICAL RULES
- **impact output is trusted.** Do NOT re-validate with cypher. Optionally run suggested grep for dynamic patterns.
- **Cite or retract.** Never state something you can't ground.
- **Read before concluding.** Don't guess from names alone.
- **Retry on failure.** If a tool fails, fix the input and try again.
- **Cyfer tool validation** prefer using cyfer tool in anything that requires graph connections.
- **OUTPUT STYLE** Prefer using tables and mermaid diagrams instead of long explanations.
- ALWAYS USE MERMAID FOR VISUALIZATION AND STRUCTURING THE OUTPUT.
- **Iterative depth.** If Function A calls Function B, read Function B. Trace logic to the source.
- **Prefer cypher** for anything requiring graph connections.
## ERROR RECOVERY
If a tool call fails (Cypher syntax, file not found, invalid regex), do NOT stop.
- Read the error, fix the input, and retry at least once.
- For Cypher errors, verify typed node labels and \`CodeRelation {type: '...'}\` filters match the GRAPH SCHEMA section above.
- If search returns nothing, try grep or a different query before concluding.
## 🎯 OUTPUT STYLE
Think like a senior architect. Be concise—no fluff, short, precise and to the point.
Think like a senior architect. Be concise — no fluff.
- Use tables for comparisons/rankings
- Use mermaid diagrams for flows/dependencies
- Use mermaid diagrams for flows, architecture, and dependencies
- Surface deep insights: patterns, coupling, design decisions
- End with **TL;DR** (short summary of the response, summing up the response and the most critical parts)
- End with **TL;DR**
## MERMAID RULES
When generating diagrams:
@@ -132,6 +156,7 @@ When generating diagrams:
- Wrap labels with spaces in quotes: A["My Label"]
- Use simple IDs: A, B, C or auth, db, api
- Flowchart: graph TD or graph LR (not flowchart)
- Keep diagrams focused — 5-10 nodes max
- Always test mentally: would this parse?
BAD: A[User's Data] --> B(Process & Save)
@@ -363,8 +388,17 @@ export type AgentMessage = { role: 'user'; content: string } | AgentHistoryMessa
export interface AgentRuntimeOptions {
/** Capture assistant/tool messages for providers that require exact transcript replay. */
captureHistory?: boolean;
/** When aborted (e.g. user clicked Stop), the stream ends with a `cancelled` chunk. */
signal?: AbortSignal;
}
const isAbortError = (error: unknown, signal?: AbortSignal): boolean => {
if (error instanceof DOMException && error.name === 'AbortError') return true;
if (error instanceof Error && error.name === 'AbortError') return true;
if (signal?.aborted) return true;
return false;
};
export const buildLangChainMessages = (messages: AgentMessage[]): BaseMessage[] =>
messages.map((message) => {
if (message.role === 'user') {
@@ -438,6 +472,7 @@ export async function* streamAgentResponse(
streamMode: ['values', 'messages'] as any,
// Allow longer tool/reasoning loops (more Cursor-like persistence)
recursionLimit: 50,
signal: options.signal,
} as any);
// Track what we've yielded to avoid duplicates
@@ -455,6 +490,10 @@ export async function* streamAgentResponse(
let lastStepMessages: any[] | null = null;
for await (const event of stream) {
if (options.signal?.aborted) {
break;
}
// Events come as [streamMode, data] tuples when using multiple modes
// or just data when using single mode
let mode: string;
@@ -511,10 +550,11 @@ export async function* streamAgentResponse(
// - After all tools are done: treat as final content
const isReasoning =
!hasSeenToolCallThisTurn || toolCalls.length > 0 || pendingToolCalls > 0;
yield {
type: isReasoning ? 'reasoning' : 'content',
[isReasoning ? 'reasoning' : 'content']: content,
};
if (isReasoning) {
yield { type: 'reasoning', reasoning: content };
} else {
yield { type: 'content', content };
}
}
// Track tool calls from message chunks
@@ -627,6 +667,11 @@ export async function* streamAgentResponse(
}
}
if (options.signal?.aborted) {
yield { type: 'cancelled' };
return;
}
// DEBUG: Stream completed normally
if (import.meta.env.DEV) {
console.log('✅ Stream completed normally, yielding done');
@@ -640,6 +685,10 @@ export async function* streamAgentResponse(
: undefined,
};
} catch (error) {
if (isAbortError(error, options.signal)) {
yield { type: 'cancelled' };
return;
}
const message = error instanceof Error ? error.message : String(error);
// DEBUG: Stream error
if (import.meta.env.DEV) {
+14
View File
@@ -16,6 +16,20 @@ import { z } from 'zod';
import { NODE_TABLES, REL_TYPES } from 'gitnexus-shared';
import type { EnrichedSearchResult, GrepResult } from '../../services/backend-client';
/**
* Tool names registered by createGraphRAGTools — kept in sync with each tool's `name`
* field (enforced by agent-prompt.test.ts) and with BASE_SYSTEM_PROMPT in agent.ts.
*/
export const GRAPH_RAG_TOOL_NAMES = [
'search',
'cypher',
'grep',
'read',
'overview',
'explore',
'impact',
] as const;
const validLabel = (label: string): boolean => (NODE_TABLES as readonly string[]).includes(label);
const validRelType = (t: string): boolean => (REL_TYPES as readonly string[]).includes(t);
+11 -16
View File
@@ -254,7 +254,7 @@ export interface ToolCallInfo {
name: string;
args: Record<string, unknown>;
result?: string;
status: 'pending' | 'running' | 'completed' | 'error';
status: 'pending' | 'running' | 'completed' | 'error' | 'stopped';
}
/**
@@ -286,22 +286,17 @@ export type AgentHistoryMessage =
};
/**
* Streaming chunk from agent
* Now supports step-based streaming where each step is a distinct message
* Streaming chunk from agent (discriminated union).
* Each variant carries only its relevant fields, enabling exhaustive switch handling.
*/
export interface AgentStreamChunk {
type: 'reasoning' | 'tool_call' | 'tool_result' | 'content' | 'error' | 'done';
/** LLM's reasoning/thinking text (shown as a step) */
reasoning?: string;
/** Final answer content (streamed token by token) */
content?: string;
/** Hidden raw transcript for reconstructing future agent turns */
historyMessages?: AgentHistoryMessage[];
/** Tool call information */
toolCall?: ToolCallInfo;
/** Error message */
error?: string;
}
export type AgentStreamChunk =
| { type: 'reasoning'; reasoning: string }
| { type: 'tool_call'; toolCall: ToolCallInfo }
| { type: 'tool_result'; toolCall: ToolCallInfo }
| { type: 'content'; content: string }
| { type: 'error'; error: string }
| { type: 'done'; historyMessages?: AgentHistoryMessage[] }
| { type: 'cancelled' };
/**
* A single step in the agent's execution
@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { GraphStateProvider, useGraphState } from './graph';
function wrapper({ children }: { children: React.ReactNode }) {
return <GraphStateProvider>{children}</GraphStateProvider>;
}
describe('GraphState', () => {
it('should have default graphViewMode as "force"', () => {
const { result } = renderHook(() => useGraphState(), { wrapper });
expect(result.current.graphViewMode).toBe('force');
});
it('should toggle graphViewMode', () => {
const { result } = renderHook(() => useGraphState(), { wrapper });
act(() => {
result.current.setGraphViewMode('tree');
});
expect(result.current.graphViewMode).toBe('tree');
});
});
+14 -1
View File
@@ -16,6 +16,8 @@ interface GraphStateContextValue {
setDepthFilter: (depth: number | null) => void;
highlightedNodeIds: Set<string>;
setHighlightedNodeIds: (ids: Set<string>) => void;
graphViewMode: 'force' | 'tree' | 'circles';
setGraphViewMode: (mode: 'force' | 'tree' | 'circles') => void;
}
const GraphStateContext = createContext<GraphStateContextValue | null>(null);
@@ -27,6 +29,7 @@ export const GraphStateProvider = ({ children }: { children: ReactNode }) => {
const [visibleEdgeTypes, setVisibleEdgeTypes] = useState<EdgeType[]>(DEFAULT_VISIBLE_EDGES);
const [depthFilter, setDepthFilter] = useState<number | null>(null);
const [highlightedNodeIds, setHighlightedNodeIds] = useState<Set<string>>(new Set());
const [graphViewMode, setGraphViewMode] = useState<'force' | 'tree' | 'circles'>('force');
const toggleLabelVisibility = useCallback((label: NodeLabel) => {
setVisibleLabels((prev) =>
@@ -54,8 +57,18 @@ export const GraphStateProvider = ({ children }: { children: ReactNode }) => {
setDepthFilter,
highlightedNodeIds,
setHighlightedNodeIds,
graphViewMode,
setGraphViewMode,
}),
[graph, selectedNode, visibleLabels, visibleEdgeTypes, depthFilter, highlightedNodeIds],
[
graph,
selectedNode,
visibleLabels,
visibleEdgeTypes,
depthFilter,
highlightedNodeIds,
graphViewMode,
],
);
return <GraphStateContext.Provider value={value}>{children}</GraphStateContext.Provider>;
+81 -13
View File
@@ -123,6 +123,10 @@ interface AppState {
depthFilter: number | null;
setDepthFilter: (depth: number | null) => void;
// Graph view mode
graphViewMode: 'force' | 'tree' | 'circles';
setGraphViewMode: (mode: 'force' | 'tree' | 'circles') => void;
// Query state
highlightedNodeIds: Set<string>;
setHighlightedNodeIds: (ids: Set<string>) => void;
@@ -232,6 +236,8 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
setDepthFilter,
highlightedNodeIds,
setHighlightedNodeIds,
graphViewMode,
setGraphViewMode,
} = useGraphState();
// Right Panel
@@ -582,6 +588,8 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
// Agent state — agent runs on main thread now (I/O-bound, not CPU-bound)
const agentRef = useRef<any>(null);
const chatAbortRef = useRef<AbortController | null>(null);
const chatStateRef = useRef<'idle' | 'streaming' | 'aborting'>('idle');
const initializeAgent = useCallback(
async (overrideProjectName?: string): Promise<void> => {
@@ -640,7 +648,7 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
const sendChatMessage = useCallback(
async (message: string): Promise<void> => {
if (isChatLoading) return;
if (chatStateRef.current !== 'idle') return;
// Refresh Code panel for the new question: keep user-pinned refs, clear old AI citations
clearAICodeReferences();
@@ -679,8 +687,13 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
}
setIsChatLoading(true);
chatStateRef.current = 'streaming';
setCurrentToolCalls([]);
chatAbortRef.current?.abort();
const chatAbortController = new AbortController();
chatAbortRef.current = chatAbortController;
const providerCapabilities = getProviderCapabilities(llmSettings.activeProvider);
// Prepare message history for agent (convert our format to AgentMessage format)
@@ -736,11 +749,13 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
});
};
let pendingUpdate = false;
let rafHandle: number | null = null;
const scheduleMessageUpdate = () => {
if (pendingUpdate) return;
pendingUpdate = true;
requestAnimationFrame(() => {
rafHandle = requestAnimationFrame(() => {
pendingUpdate = false;
rafHandle = null;
updateMessage();
});
};
@@ -887,7 +902,7 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
if (idx < 0) {
idx = toolCallsForMessage.findIndex((t) => t.name === tc.name && !t.result);
}
if (idx >= 0) {
if (idx >= 0 && toolCallsForMessage[idx].status !== 'stopped') {
toolCallsForMessage[idx] = {
...toolCallsForMessage[idx],
result: tc.result,
@@ -903,7 +918,11 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
(s.toolCall.id === tc.id ||
(s.toolCall.name === tc.name && s.toolCall.status === 'running')),
);
if (stepIdx >= 0 && stepsForMessage[stepIdx].toolCall) {
if (
stepIdx >= 0 &&
stepsForMessage[stepIdx].toolCall &&
stepsForMessage[stepIdx].toolCall!.status !== 'stopped'
) {
stepsForMessage[stepIdx] = {
...stepsForMessage[stepIdx],
toolCall: {
@@ -924,6 +943,8 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
targetIdx = prev.findIndex((t) => t.name === tc.name && !t.result);
}
if (targetIdx >= 0) {
const target = prev[targetIdx];
if (target.status === 'stopped') return prev;
return prev.map((t, i) =>
i === targetIdx ? { ...t, result: tc.result, status: 'completed' } : t,
);
@@ -1022,13 +1043,24 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
const { streamAgentResponse } = await import('../core/llm/agent');
for await (const chunk of streamAgentResponse(agent, history, {
captureHistory: providerCapabilities.preserveAssistantTranscript,
signal: chatAbortController.signal,
})) {
if (chunk.type === 'cancelled') {
break;
}
onChunk(chunk);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setAgentError(message);
if (!chatAbortController.signal.aborted) {
const message = error instanceof Error ? error.message : String(error);
setAgentError(message);
}
} finally {
if (rafHandle != null) {
cancelAnimationFrame(rafHandle);
rafHandle = null;
}
chatStateRef.current = 'idle';
setIsChatLoading(false);
setCurrentToolCalls([]);
}
@@ -1044,22 +1076,56 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
clearAIToolHighlights,
graph,
embeddingStatus,
isChatLoading,
],
);
const stopChatResponse = useCallback(() => {
if (isChatLoading) {
// Agent streaming will be interrupted by the AbortController in sendChatMessage
setIsChatLoading(false);
setCurrentToolCalls([]);
}
}, [isChatLoading]);
if (!chatAbortRef.current) return;
chatStateRef.current = 'aborting';
chatAbortRef.current.abort();
chatAbortRef.current = null;
const stoppedLabel = i18n.t('chat:stopped');
const markStoppedToolCall = (tc: ToolCallInfo): ToolCallInfo =>
tc.status === 'running' || tc.status === 'pending'
? { ...tc, status: 'stopped', result: stoppedLabel }
: tc;
setCurrentToolCalls((prev) => prev.map(markStoppedToolCall));
setChatMessages((prev) => {
const lastAssistantIdx = [...prev]
.map((m, i) => (m.role === 'assistant' ? i : -1))
.filter((i) => i >= 0)
.pop();
if (lastAssistantIdx === undefined) return prev;
const message = prev[lastAssistantIdx];
const updated: ChatMessage = {
...message,
toolCalls: message.toolCalls?.map(markStoppedToolCall),
steps: message.steps?.map((step) =>
step.type === 'tool_call' && step.toolCall
? { ...step, toolCall: markStoppedToolCall(step.toolCall) }
: step,
),
};
return prev.map((m, i) => (i === lastAssistantIdx ? updated : m));
});
setIsChatLoading(false);
}, []);
const clearChat = useCallback(() => {
chatAbortRef.current?.abort();
chatAbortRef.current = null;
chatStateRef.current = 'idle';
setChatMessages([]);
setCurrentToolCalls([]);
setAgentError(null);
setIsChatLoading(false);
}, []);
// Switch to a different repo on the connected server
@@ -1266,6 +1332,8 @@ const AppStateProviderInner = ({ children }: { children: ReactNode }) => {
toggleEdgeVisibility,
depthFilter,
setDepthFilter,
graphViewMode,
setGraphViewMode,
highlightedNodeIds,
setHighlightedNodeIds,
aiCitationHighlightedNodeIds,
File diff suppressed because it is too large Load Diff
+307
View File
@@ -0,0 +1,307 @@
import type { KnowledgeGraph } from '../core/graph/types';
import type { GraphNode, NodeLabel } from 'gitnexus-shared';
import { NODE_SIZES } from './constants';
export interface CirclesNodePosition {
x: number;
y: number;
size: number;
/** Logical ring index 0 (innermost) … RING_COUNT-1 (outermost) */
ring: number;
/** Angle in radians, stored so the physics can use it as an anchor */
angle: number;
}
// ---------------------------------------------------------------------------
// Configurable constants
// ---------------------------------------------------------------------------
/** Target radius (px) for each ring. Ring 0 is innermost. */
export const CIRCLES_RING_RADII = [90, 240, 420, 620] as const;
/**
* Half-width of the allowed radial band around each ring centre.
* Keep this small enough that adjacent rings never overlap.
* Current ring gaps: 150 / 180 / 200 px → band = 45 leaves 60-110 px of clear air.
*/
export const CIRCLES_BAND_HALF = 45;
/** Number of rings (= number of layers). */
export const RING_COUNT = CIRCLES_RING_RADII.length; // 4
// ---------------------------------------------------------------------------
// Layer assignment — identical to tree-layout so the same node types
// end up in the same conceptual layer.
// ---------------------------------------------------------------------------
const TYPE_TO_RING: Record<string, number> = {
// Ring 0 – innermost: structural containers
Project: 0,
Package: 0,
Module: 0,
Folder: 0,
Namespace: 0,
// Ring 1 – files
File: 1,
Section: 1,
Import: 1,
Route: 1,
Tool: 1,
// Ring 2 – type definitions
Class: 2,
Interface: 2,
Enum: 2,
Type: 2,
Struct: 2,
Trait: 2,
Union: 2,
Record: 2,
Typedef: 2,
Template: 2,
TypeAlias: 2,
// Ring 3 – outermost: functions / methods / variables
Function: 3,
Method: 3,
Impl: 3,
Delegate: 3,
Constructor: 3,
Variable: 3,
Const: 3,
Static: 3,
Property: 3,
Decorator: 3,
Annotation: 3,
Macro: 3,
CodeElement: 3,
};
const DEFAULT_RING = 1;
/** Hierarchy edges used for angular-allocation grouping. */
export const CIRCLES_HIERARCHY_RELATIONS = new Set([
'CONTAINS',
'DEFINES',
'HAS_METHOD',
'HAS_PROPERTY',
]);
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
function getNodeRing(node: GraphNode): number {
return TYPE_TO_RING[node.label] ?? DEFAULT_RING;
}
function calculateNodeSize(ring: number, nodeType: NodeLabel): number {
const baseSize = NODE_SIZES[nodeType] || 6;
const ringMultiplier = Math.max(0.6, 1 - ring * 0.12);
return baseSize * ringMultiplier;
}
function deterministicHash(str: string): number {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) + hash + str.charCodeAt(i);
hash |= 0;
}
return (Math.abs(hash) % 10000) / 10000;
}
function buildHierarchyMaps(graph: KnowledgeGraph) {
const childrenByParent = new Map<string, string[]>();
const parentsByChild = new Map<string, string[]>();
for (const rel of graph.relationships) {
if (!CIRCLES_HIERARCHY_RELATIONS.has(rel.type)) continue;
if (!childrenByParent.has(rel.sourceId)) childrenByParent.set(rel.sourceId, []);
childrenByParent.get(rel.sourceId)!.push(rel.targetId);
if (!parentsByChild.has(rel.targetId)) parentsByChild.set(rel.targetId, []);
parentsByChild.get(rel.targetId)!.push(rel.sourceId);
}
return { childrenByParent, parentsByChild };
}
// ---------------------------------------------------------------------------
// Parent-centred angular allocation
//
// Each parent's children are placed in an arc CENTRED on the parent's own
// angle, with arc size proportional to child count. This prevents the
// sequential-concatenation bias (where the largest group's arc centre drifts
// to 90° / 270° regardless of where the parent sits) that caused top-bottom
// crowding in the previous sequential allocation.
//
// Overlapping initial arcs are fine — the physics simulation's angular spread
// force resolves them during the simulation.
// ---------------------------------------------------------------------------
function initParentCentredAngles(
graph: KnowledgeGraph,
parentsByChild: Map<string, string[]>,
): Map<string, CirclesNodePosition> {
const positions = new Map<string, CirclesNodePosition>();
// Group nodes by ring
const nodesByRing: GraphNode[][] = Array.from({ length: RING_COUNT }, () => []);
const nodeRingMap = new Map<string, number>();
for (const node of graph.nodes) {
const ring = getNodeRing(node);
if (ring >= 0 && ring < RING_COUNT) {
nodesByRing[ring].push(node);
nodeRingMap.set(node.id, ring);
}
}
const TWO_PI = Math.PI * 2;
// --- Ring 0: sorted alphabetically, evenly spaced around full circle ---
const ring0Nodes = [...nodesByRing[0]].sort((a, b) =>
a.properties.name.localeCompare(b.properties.name),
);
if (ring0Nodes.length > 0) {
const count = ring0Nodes.length;
for (let i = 0; i < count; i++) {
const node = ring0Nodes[i];
const angle = (i / count) * TWO_PI;
const r = CIRCLES_RING_RADII[0];
positions.set(node.id, {
x: r * Math.cos(angle),
y: r * Math.sin(angle),
size: calculateNodeSize(0, node.label),
ring: 0,
angle,
});
}
}
// --- Rings 1-3: parent-centred arc placement ---
for (let ring = 1; ring < RING_COUNT; ring++) {
const ringNodes = nodesByRing[ring];
if (ringNodes.length === 0) continue;
const r = CIRCLES_RING_RADII[ring];
// Find each node's primary parent: placed ancestor with highest ring index
// (so a Method prefers its Class over a distant Package).
const assignedParent = new Map<string, string>();
for (const node of ringNodes) {
const parents = parentsByChild.get(node.id) ?? [];
let bestParent: string | null = null;
let bestParentRing = -1;
for (const p of parents) {
if (!positions.has(p)) continue;
const pRing = nodeRingMap.get(p) ?? -1;
if (pRing > bestParentRing) {
bestParentRing = pRing;
bestParent = p;
}
}
if (bestParent) assignedParent.set(node.id, bestParent);
}
// Bucket into parent groups and orphans
const childrenOfParent = new Map<string, GraphNode[]>();
const orphans: GraphNode[] = [];
for (const node of ringNodes) {
const p = assignedParent.get(node.id);
if (!p) {
orphans.push(node);
} else {
if (!childrenOfParent.has(p)) childrenOfParent.set(p, []);
childrenOfParent.get(p)!.push(node);
}
}
for (const children of childrenOfParent.values()) {
children.sort((a, b) => a.properties.name.localeCompare(b.properties.name));
}
orphans.sort((a, b) => a.properties.name.localeCompare(b.properties.name));
const totalParented = ringNodes.length - orphans.length;
const parentedFraction = totalParented > 0 ? totalParented / ringNodes.length : 0;
// Place each parent's children in an arc centred on the parent's angle.
// Arc size ∝ child count relative to all parented nodes.
for (const [parentId, children] of childrenOfParent) {
if (children.length === 0) continue;
const parentAngle = positions.get(parentId)?.angle ?? 0;
const slotArc = (children.length / totalParented) * parentedFraction * TWO_PI;
const startAngle = parentAngle - slotArc / 2;
for (let i = 0; i < children.length; i++) {
const angle = startAngle + (i + 0.5) * (slotArc / children.length);
positions.set(children[i].id, {
x: r * Math.cos(angle),
y: r * Math.sin(angle),
size: calculateNodeSize(ring, children[i].label),
ring,
angle,
});
}
}
// Orphans: spread evenly in their proportional arc, centred at angle = π
// (left side), away from the 0° / ±π boundary to avoid wrapping artefacts.
if (orphans.length > 0) {
const orphanFraction = orphans.length / ringNodes.length;
const orphanArc = orphanFraction * TWO_PI;
// Centre orphan arc at π so it doesn't overlap with the typical 0° cluster
const orphanStart = Math.PI - orphanArc / 2;
for (let i = 0; i < orphans.length; i++) {
const angle = orphanStart + (i + 0.5) * (orphanArc / orphans.length);
positions.set(orphans[i].id, {
x: r * Math.cos(angle),
y: r * Math.sin(angle),
size: calculateNodeSize(ring, orphans[i].label),
ring,
angle,
});
}
}
}
return positions;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Circles view layout: concentric rings with parent-centred angular allocation.
*
* Ring 0 (innermost) = Folders/Packages
* Ring 1 = Files
* Ring 2 = Classes/Interfaces
* Ring 3 (outermost) = Functions/Methods/Variables
*
* Returns initial positions; the physics simulation in useSigma.ts refines
* them using radial gravity + hard band clamping, angular spread, and 2D
* repulsion — identical in structure to the tree-view physics.
*/
export function calculateCirclesLayout(graph: KnowledgeGraph): Map<string, CirclesNodePosition> {
const { parentsByChild } = buildHierarchyMaps(graph);
// 1. Parent-centred angular allocation — no top/bottom bias
const positions = initParentCentredAngles(graph, parentsByChild);
// 2. Subtle radial jitter only — angular jitter would fight the centred placement
for (const [nodeId, pos] of positions) {
const jitter = (deterministicHash(nodeId + 'r') - 0.5) * 10; // ±10 px
const r = CIRCLES_RING_RADII[pos.ring] + jitter;
pos.x = r * Math.cos(pos.angle);
pos.y = r * Math.sin(pos.angle);
}
return positions;
}
+7 -1
View File
@@ -101,7 +101,9 @@ export const getCommunityColor = (communityIndex: number): string => {
return COMMUNITY_COLORS[communityIndex % COMMUNITY_COLORS.length];
};
// Labels to show by default (hide imports and variables by default as they clutter)
// Labels to show by default (hide imports by default as they clutter).
// Property/Const are the Kotlin/Java equivalents of Variable — include them so
// Kotlin repos don't appear to have no leaf nodes.
export const DEFAULT_VISIBLE_LABELS: NodeLabel[] = [
'Project',
'Package',
@@ -111,6 +113,8 @@ export const DEFAULT_VISIBLE_LABELS: NodeLabel[] = [
'Class',
'Function',
'Method',
'Property', // Kotlin/Java fields (HAS_PROPERTY + DEFINES File→Property)
'Const', // Top-level constants
'Interface',
'Enum',
'Type',
@@ -127,6 +131,8 @@ export const FILTERABLE_LABELS: NodeLabel[] = [
'Function',
'Method',
'Variable',
'Property', // Kotlin/Java field nodes
'Const',
'Decorator',
'Import',
];
+208
View File
@@ -0,0 +1,208 @@
import { describe, it, expect } from 'vitest';
import { knowledgeGraphToTreeGraphology, knowledgeGraphToCirclesGraphology } from './graph-adapter';
import type { KnowledgeGraph } from '../core/graph/types';
import type { GraphNode } from 'gitnexus-shared';
import { EDGE_INFO } from './constants';
function makeNode(id: string, label: string, name: string): GraphNode {
return {
id,
label: label as any,
properties: { name, filePath: '', startLine: 1, endLine: 1 },
};
}
describe('knowledgeGraphToTreeGraphology', () => {
it('should create a graph with tree layout', () => {
const graph: KnowledgeGraph = {
nodes: [
makeNode('root', 'Project', 'MyProject'),
makeNode('folder', 'Folder', 'src'),
makeNode('file', 'File', 'main.ts'),
],
relationships: [
{ id: 'r1', type: 'CONTAINS', sourceId: 'root', targetId: 'folder' },
{ id: 'r2', type: 'CONTAINS', sourceId: 'folder', targetId: 'file' },
{ id: 'r3', type: 'CALLS', sourceId: 'file', targetId: 'root' },
],
};
const sigmaGraph = knowledgeGraphToTreeGraphology(graph);
expect(sigmaGraph.hasNode('root')).toBe(true);
expect(sigmaGraph.hasNode('folder')).toBe(true);
expect(sigmaGraph.hasNode('file')).toBe(true);
const rootAttrs = sigmaGraph.getNodeAttributes('root');
const folderAttrs = sigmaGraph.getNodeAttributes('folder');
const fileAttrs = sigmaGraph.getNodeAttributes('file');
// Tree view is inverted vertically, so files sit above containers.
expect(fileAttrs.y).toBeLessThan(rootAttrs.y);
expect(fileAttrs.y).toBeLessThan(folderAttrs.y);
// Nodes should have reasonable sizes
expect(rootAttrs.size).toBeGreaterThan(2);
expect(folderAttrs.size).toBeGreaterThan(2);
expect(fileAttrs.size).toBeGreaterThan(2);
expect(rootAttrs.treeAnchorX).toBe(rootAttrs.x);
expect(rootAttrs.treeAnchorY).toBe(rootAttrs.y);
expect(rootAttrs.treeLayer).toBe(0);
expect(fileAttrs.treeLayer).toBe(1);
});
it('should style hierarchy edges differently from cross-cutting edges', () => {
const graph: KnowledgeGraph = {
nodes: [makeNode('a', 'Function', 'fnA'), makeNode('b', 'Function', 'fnB')],
relationships: [
{ id: 'r1', type: 'CONTAINS', sourceId: 'a', targetId: 'b' },
{ id: 'r2', type: 'CALLS', sourceId: 'a', targetId: 'b' },
],
};
const sigmaGraph = knowledgeGraphToTreeGraphology(graph);
// MultiGraph allows multiple edges per pair — both CONTAINS and CALLS must survive.
expect(sigmaGraph.size).toBe(2);
const attrsByType = new Map<string, { isHierarchyEdge?: boolean; color: string }>();
sigmaGraph.forEachEdge((_edge, attrs) => {
attrsByType.set(attrs.relationType, attrs);
});
const containsAttrs = attrsByType.get('CONTAINS');
expect(containsAttrs).toBeDefined();
expect(containsAttrs!.isHierarchyEdge).toBe(true);
expect(containsAttrs!.color).toBe(EDGE_INFO.CONTAINS.color);
const callsAttrs = attrsByType.get('CALLS');
expect(callsAttrs).toBeDefined();
expect(callsAttrs!.isHierarchyEdge).toBe(false);
expect(callsAttrs!.color).toBe(EDGE_INFO.CALLS.color);
});
it('should treat imports as cross-cutting edges in tree view', () => {
const graph: KnowledgeGraph = {
nodes: [makeNode('a', 'File', 'a.ts'), makeNode('b', 'File', 'b.ts')],
relationships: [{ id: 'r1', type: 'IMPORTS', sourceId: 'a', targetId: 'b' }],
};
const sigmaGraph = knowledgeGraphToTreeGraphology(graph);
sigmaGraph.forEachEdge((edge, attrs) => {
if (attrs.relationType === 'IMPORTS') {
expect(attrs.isHierarchyEdge).toBe(false);
expect(attrs.color).toBe(EDGE_INFO.IMPORTS.color);
}
});
});
it('should handle a medium-sized graph without dropping nodes or edges', () => {
// 2000 nodes + 4000 edges — exercises the adaptive spring iteration path (14 iters).
// Structural assertion only: wall-clock timing is too variable across CI machines.
const nodes: GraphNode[] = Array.from({ length: 2000 }, (_, i) =>
makeNode(`n${i}`, i % 4 === 0 ? 'Folder' : i % 4 === 1 ? 'File' : 'Function', `node${i}`),
);
const relationships = Array.from({ length: 4000 }, (_, i) => ({
id: `r${i}`,
type: i % 3 === 0 ? 'CONTAINS' : 'CALLS',
sourceId: `n${i % 2000}`,
targetId: `n${(i + 7) % 2000}`,
}));
const graph: KnowledgeGraph = { nodes, relationships };
const sigmaGraph = knowledgeGraphToTreeGraphology(graph);
// All nodes that have a tree-layout position must be present in the output.
expect(sigmaGraph.order).toBe(2000);
// Every relationship whose source and target both exist should produce an edge.
// Self-loops (sourceId === targetId) are excluded — the adapter skips them.
const selfLoops = relationships.filter((r) => r.sourceId === r.targetId).length;
expect(sigmaGraph.size).toBe(relationships.length - selfLoops);
});
});
describe('knowledgeGraphToCirclesGraphology', () => {
it('should place nodes into ring positions based on their type', () => {
const graph: KnowledgeGraph = {
nodes: [
makeNode('folder', 'Folder', 'src'),
makeNode('file', 'File', 'main.ts'),
makeNode('fn', 'Function', 'doSomething'),
],
relationships: [
{ id: 'r1', type: 'CONTAINS', sourceId: 'folder', targetId: 'file' },
{ id: 'r2', type: 'CALLS', sourceId: 'file', targetId: 'fn' },
],
};
const sigmaGraph = knowledgeGraphToCirclesGraphology(graph);
expect(sigmaGraph.hasNode('folder')).toBe(true);
expect(sigmaGraph.hasNode('file')).toBe(true);
expect(sigmaGraph.hasNode('fn')).toBe(true);
// Each node carries its ring index and anchor coordinates
const folderAttrs = sigmaGraph.getNodeAttributes('folder');
const fileAttrs = sigmaGraph.getNodeAttributes('file');
const fnAttrs = sigmaGraph.getNodeAttributes('fn');
expect(typeof folderAttrs.circlesRing).toBe('number');
expect(typeof folderAttrs.circlesAnchorX).toBe('number');
expect(typeof folderAttrs.circlesAnchorY).toBe('number');
// Folders/Packages live in ring 0 (innermost); Files in ring 1; Functions in ring 3.
expect(folderAttrs.circlesRing).toBe(0);
expect(fileAttrs.circlesRing).toBe(1);
expect(fnAttrs.circlesRing).toBe(3);
// Tree anchor attributes must NOT be set in circles mode
expect(folderAttrs.treeAnchorX).toBeUndefined();
expect(folderAttrs.treeAnchorY).toBeUndefined();
});
it('should style hierarchy edges differently from cross-cutting edges', () => {
const graph: KnowledgeGraph = {
nodes: [makeNode('a', 'File', 'a.ts'), makeNode('b', 'Function', 'fn')],
relationships: [
{ id: 'r1', type: 'CONTAINS', sourceId: 'a', targetId: 'b' },
{ id: 'r2', type: 'CALLS', sourceId: 'a', targetId: 'b' },
],
};
// MultiGraph allows multiple edges per pair — both CONTAINS and CALLS must survive.
const sigmaGraph = knowledgeGraphToCirclesGraphology(graph);
expect(sigmaGraph.size).toBe(2);
const attrsByType = new Map<string, { isHierarchyEdge?: boolean; color: string }>();
sigmaGraph.forEachEdge((_, attrs) => {
attrsByType.set(attrs.relationType, attrs);
});
const containsAttrs = attrsByType.get('CONTAINS');
expect(containsAttrs).toBeDefined();
expect(containsAttrs!.isHierarchyEdge).toBe(true);
expect(containsAttrs!.color).toBe(EDGE_INFO.CONTAINS.color);
const callsAttrs = attrsByType.get('CALLS');
expect(callsAttrs).toBeDefined();
expect(callsAttrs!.isHierarchyEdge).toBe(false);
expect(callsAttrs!.color).toBe(EDGE_INFO.CALLS.color);
});
it('should treat CALLS as a cross-cutting edge in circles view', () => {
const graph: KnowledgeGraph = {
nodes: [makeNode('a', 'Function', 'fnA'), makeNode('b', 'Function', 'fnB')],
relationships: [{ id: 'r1', type: 'CALLS', sourceId: 'a', targetId: 'b' }],
};
const sigmaGraph = knowledgeGraphToCirclesGraphology(graph);
sigmaGraph.forEachEdge((_, attrs) => {
expect(attrs.isHierarchyEdge).toBe(false);
expect(attrs.color).toBe(EDGE_INFO.CALLS.color);
});
});
});

Some files were not shown because too many files have changed in this diff Show More