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
518 changed files with 23682 additions and 29562 deletions
@@ -16,10 +16,10 @@ 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 `node .gitnexus/run.cjs analyze` in terminal.
@@ -28,11 +28,11 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
```
- [ ] 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,8 +18,8 @@ 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
```
@@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
```
- [ ] 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
@@ -17,9 +17,9 @@ 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
```
@@ -28,11 +28,11 @@ description: "Use when the user wants to know what will break if they change som
## 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,10 +18,10 @@ 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
```
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] 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,9 +16,9 @@ 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
```
@@ -29,65 +29,65 @@ description: "Use when the user wants to rename, extract, split, move, or restru
### 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
```
+4
View File
@@ -0,0 +1,4 @@
# Code owners
* @Arvuno
* @magyargergo
-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
+6 -24
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
@@ -117,14 +109,15 @@ jobs:
# 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.
@@ -137,14 +130,3 @@ jobs:
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with:
sarif_file: zizmor.sarif
category: zizmor
+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''',
]
+8 -9
View File
@@ -39,8 +39,7 @@ 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.
@@ -81,18 +80,18 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati
## 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
+8 -8
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
@@ -62,18 +62,18 @@ This project is indexed by GitNexus as **GitNexus** (26675 symbols, 35395 relati
## 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
+2
View File
@@ -13,6 +13,8 @@ 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`
+37 -2
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>
@@ -226,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 [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
@@ -273,13 +278,43 @@ 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. |
+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.
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]]
@@ -27,6 +27,8 @@
*/
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const NPX_REF = 'gitnexus@latest';
@@ -34,46 +36,72 @@ const NPX_REF = 'gitnexus@latest';
const PNPM_ALLOW_BUILD_BASE = ['@ladybugdb/core', 'gitnexus', 'tree-sitter'];
const PNPM_ALLOW_BUILD_EMBEDDINGS = ['onnxruntime-node'];
// Probe timeout, kept under Claude Code's 10s hook budget. 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 four 1s probes
// (which gitnexus, npm --version, which pnpm, pnpm --version), so the worst case
// is ~9s — within budget but tight. A healthy `which`/`where`/`--version`
// returns in well under a second, so the realistic cost is far lower.
// 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;
/**
* Pick the best match from `where`/`which` output. A global `gitnexus` may be a
* `.cmd`/`.bat` (npm), a `.exe`, or an extensionless shim (Volta, scoop), so on
* Windows we prefer a recognized executable extension but accept any hit — the
* emitted hint is `gitnexus analyze` regardless of which shim resolves it. Pure
* and exported so the shim-matching can be unit-tested without spawning.
* 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 pickPathMatch(output, { isWin, gitnexusWrapper } = {}) {
const lines = output
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
if (isWin && gitnexusWrapper) {
return lines.find((l) => /\.(cmd|bat|exe)$/i.test(l)) || lines[0] || null;
}
return lines[0] || null;
}
/** Absolute path to `command` on PATH, or null. `gitnexusWrapper` enables the Windows shim match. */
function resolveOnPath(command, gitnexusWrapper = false) {
const isWin = process.platform === 'win32';
try {
const output = execFileSync(isWin ? 'where' : 'which', [command], {
encoding: 'utf-8',
timeout: PROBE_TIMEOUT_MS,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
});
return pickPathMatch(output, { isWin, gitnexusWrapper });
} catch {
return null;
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
@@ -193,12 +221,12 @@ function formatPnpmDlxCommand(gitnexusArgs, options = {}, deps = {}) {
function formatAnalyzeCommand(options = {}, deps = {}) {
const suffix = options.embeddings ? ' --embeddings' : '';
// Keep the stale-index hook budget tight by querying each tool at most once.
// A memoized PATH probe is shared with resolveInvocationMode (so `gitnexus`
// isn't probed twice), and pnpm's version is captured by a single
// `pnpm --version` that proves both presence (for mode resolution) and
// version (for the allow-build gate) — replacing the former `which pnpm` +
// `pnpm --version` double spawn. Injected deps (tests) and forced/global
// modes skip the pnpm probe.
// 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}`;
@@ -256,7 +284,7 @@ module.exports = {
formatPnpmDlxCommand,
resolveInvocationMode,
buildRunnerArgv,
pickPathMatch,
resolveOnPath,
getNpmMajorVersion,
NPX_REF,
PNPM_ALLOW_BUILD_BASE,
@@ -16,10 +16,10 @@ 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 `node .gitnexus/run.cjs analyze` in terminal.
@@ -28,11 +28,11 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
```
- [ ] 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,8 +18,8 @@ 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
```
@@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
```
- [ ] 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
@@ -17,9 +17,9 @@ 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
```
@@ -28,11 +28,11 @@ description: "Use when the user wants to know what will break if they change som
## 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,10 +18,10 @@ 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
```
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] 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,9 +16,9 @@ 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
```
@@ -29,65 +29,65 @@ description: "Use when the user wants to rename, extract, split, move, or restru
### 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
```
@@ -15,10 +15,10 @@ 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 `node .gitnexus/run.cjs analyze` in terminal.
@@ -27,11 +27,11 @@ description: Trace bugs through call chains using knowledge graph
```
- [ ] 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,8 +17,8 @@ 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
```
@@ -28,9 +28,9 @@ description: Navigate unfamiliar code using GitNexus knowledge graph
```
- [ ] 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,9 +16,9 @@ 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
```
@@ -27,11 +27,11 @@ description: Analyze blast radius before making code changes
## 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,10 +18,10 @@ 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
```
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] 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,9 +15,9 @@ 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
```
@@ -27,60 +27,60 @@ description: Plan safe refactors using blast radius and dependency mapping
### 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';
@@ -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
@@ -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];
}
@@ -59,4 +59,13 @@ export interface SymbolDefinition {
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;
}
@@ -439,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[];
}
+48 -86
View File
@@ -18,7 +18,7 @@
"@tailwindcss/vite": "^4.3.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",
@@ -57,7 +57,7 @@
"@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",
@@ -1965,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": {
@@ -2430,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": {
@@ -2933,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": {
@@ -2959,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": {
@@ -2986,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": {
@@ -2996,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",
@@ -3060,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": {
@@ -3331,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"
},
@@ -3598,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",
@@ -4396,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",
@@ -4483,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"
@@ -5029,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",
@@ -5334,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",
@@ -5790,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"
@@ -5804,7 +5766,7 @@
"node": ">=20"
},
"peerDependencies": {
"@langchain/core": "^1.1.42"
"@langchain/core": "^1.1.48"
}
},
"node_modules/langsmith": {
@@ -6142,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"
@@ -8350,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": {
+5 -5
View File
@@ -28,7 +28,7 @@
"@tailwindcss/vite": "^4.3.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",
@@ -67,7 +67,7 @@
"@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",
+68 -43
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)
+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);
@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import { BASE_SYSTEM_PROMPT } from '../../src/core/llm/agent';
import {
createGraphRAGTools,
GRAPH_RAG_TOOL_NAMES,
type GraphRAGBackend,
} from '../../src/core/llm/tools';
import { NODE_REF_REGEX } from '../../src/lib/grounding-patterns';
/** Legacy or phantom tool names that must not appear in the system prompt. */
const FORBIDDEN_TOOL_NAMES = [
'hybrid_search',
'semantic_search',
'semantic_search_with_context',
'execute_cypher',
'execute_vector_cypher',
'grep_code',
'read_file',
'get_graph_schema',
'get_code_content',
'get_codebase_stats',
] as const;
/**
* No-op backend. createGraphRAGTools only captures these methods inside each tool's
* async execute closure — it never invokes them at construction time — so empty
* implementations are enough to build the tools and read their registered names.
*/
const stubBackend: GraphRAGBackend = {
executeQuery: async () => [],
search: async () => [],
grep: async () => [],
readFile: async () => '',
};
describe('BASE_SYSTEM_PROMPT tool parity', () => {
it('documents every registered Graph RAG tool by exact name', () => {
for (const name of GRAPH_RAG_TOOL_NAMES) {
expect(BASE_SYSTEM_PROMPT).toContain(`\`${name}\``);
}
});
it('keeps GRAPH_RAG_TOOL_NAMES in sync with the tools createGraphRAGTools registers', () => {
const registered = createGraphRAGTools(stubBackend).map((t) => t.name);
expect(registered.sort()).toEqual([...GRAPH_RAG_TOOL_NAMES].sort());
});
it('does not reference legacy or non-existent tool names', () => {
for (const name of FORBIDDEN_TOOL_NAMES) {
// Word-boundary match catches both backticked and bare-prose mentions.
expect(BASE_SYSTEM_PROMPT).not.toMatch(new RegExp(`\\b${name}\\b`));
}
});
it('uses explicit file citation format expected by the UI parser', () => {
expect(BASE_SYSTEM_PROMPT).toMatch(/\[\[src\/[^\]]+:\d+-\d+\]\]/);
expect(BASE_SYSTEM_PROMPT).not.toContain('[[file:line]]');
});
it('documents a parser-recognized symbol citation format', () => {
// Use the UI parser's own allowlist (NODE_REF_REGEX) so this tracks the parser
// instead of forking its label list. NODE_REF_REGEX is /g; use a non-global copy
// so the match is stateless.
expect(BASE_SYSTEM_PROMPT).toMatch(new RegExp(NODE_REF_REGEX.source));
});
it('documents typed node labels, not polymorphic CodeNode', () => {
expect(BASE_SYSTEM_PROMPT).toContain('MATCH (f:Function)');
expect(BASE_SYSTEM_PROMPT).not.toContain('CodeNode');
expect(BASE_SYSTEM_PROMPT).not.toContain('INHERITS');
});
it('clarifies highlight_in_graph is not a callable tool', () => {
// Reword-proof, registry-level guarantee: the load-bearing fact is that
// highlight_in_graph is not a registered tool, regardless of prompt phrasing.
expect(GRAPH_RAG_TOOL_NAMES).not.toContain('highlight_in_graph');
// The prompt still addresses it explicitly...
expect(BASE_SYSTEM_PROMPT).toContain('highlight_in_graph');
// ...and must never instruct the model to call it (guards an affirmative reword).
expect(BASE_SYSTEM_PROMPT).not.toMatch(/\b(?:use|call|invoke)\s+`?highlight_in_graph/i);
});
});
+65
View File
@@ -4,6 +4,71 @@ All notable changes to GitNexus will be documented in this file.
## [Unreleased]
## [1.6.6] - 2026-06-08
### Added
- **Scope-resolution (RFC #909) migrations completed across the language matrix** — Rust (#1639), JavaScript (#1640), Ruby (#1831), Swift (#937, #1948), Vue SFC (#940, #1950), Dart (#939, #1970), COBOL (#941, #1835, #1842), and Kotlin (#1727, #1746, #1782) now run on the registry-primary path; Java reached 100% scope-resolution parity and joined `MIGRATED_LANGUAGES` (#1805); per-language progress reporting added to the scope-resolution phase (#1813)
- **HTTP route & consumer contract extraction (group mode)** — Spring interface routes attributed to controllers (#1743); named/positional Java Spring route args (#1834); Kotlin Spring HTTP route, consumer, and WebClient long-form extraction (#1849, #1855, #1884); Java HTTP consumer contracts (#1872); OpenFeign `@RequestLine` consumer contracts incl. plain interfaces without `@FeignClient` (#1904, #1917); FastAPI `include_router(prefix=...)` cross-file routes (#1877); indirect call patterns via FastAPI `Depends()` and frontend HTTP consumers (#1852); gRPC consumer FQN derivation from Java imports for client-jar consumers (#1889)
- **C++ overload & template resolution** — operator-call resolution (#1754), template partial ordering (#1885), user-defined conversion ranking (#1829), nullptr/ellipsis pointer conversion ranks (#1708), SFINAE filter (#1623), expanded `type_traits` constraint registry (#1648), structured resolver-suppression outcomes (#1785), function-type ADL entities (#1822), and a parameter-type class sidecar (#1642)
- **Go enhancements** — structural interface implementation inference (#1966) and a `builtInNames` set for the Go language provider (#1886)
- **Self-healing worker pool** — automatic worker replacement plus deferred-resolution observability and verbose progress logging (#1741, #1773, #1947)
- **`.gitnexusrc` config file and `gitnexus analyze --default-branch`** (#243, #1996)
- **CLI / MCP impact ergonomics** — `--uid/--file/--kind` disambiguation flags (#1907, #1914), `limit/offset/summaryOnly` pagination on the impact tool (#1818), and a per-symbol `processes` field on `byDepth` items (#1867)
- **`gitnexus analyze --repair-fts`** — enforces FTS verification with hardened repair safeguards (#1720)
- **Web viewer** — Tree View and Circles View (#1799), GitLab repository URLs (#1565), `GITNEXUS_BACKEND_URL` env var for Docker deployments (#1286), and web + CLI internationalization (#1748)
- **Wiki** — local Claude/Codex providers (#1769), an opencode local provider (#2039), and `gitnexus wiki --lang <lang>` for multilanguage wiki generation (#1613)
- **`detect-changes` git-worktree support** (#1654)
- **DeepSeek V4 API support** (#1594)
- **Devcontainer for the Claude / Codex / Cursor CLIs** (#1875) and antigravity integration setup + hook adapter (#1730)
- **Object-literal methods linked to exported bindings** (#1718)
- **`eval-server --host`** for a user-configured bind IP (#1667)
- **PR reviewer swarm agents** (#1851)
- **tree-sitter node-type/field validation gate** — validates against the grammar and removes dead literal handling (#1937)
### Fixed
- **Parsing-layer coverage gaps closed across the language matrix** (umbrella #1919) — remaining open gaps (#2072) plus Java F35/F38/F41 (#1928, #2045), PHP F53/F54/F55 (#1931, #1989), COBOL F17–F23 (#1925, #1959), Rust F66/F68/F71/F72 (#1934, #1974), Python F57/F58/F61 (#1932, #1964), JS/TS F44/F83/F85/F86/F87 (#1929, #1968), and Ruby F62 (#1933, #1972)
- **Fully-qualified nested-type identity for C++ and Ruby** — distinct nodes for union-, anonymous-namespace-, and same-tail-nested types (#1978, #1981, #2004, #2005); cross-namespace same-tail inheritance bases resolved (#1993, #2005); Ruby same-tail nested mixin modules qualified with `IMPLEMENTS` routed by scope (#1991, #2006); shared codec for `__heritage__`/`__property__` markers (#1994, #2007); graph nodes materialized for scoped class/module/impl declarations (#1975, #1977); generic Rust inherent-impl methods owned through the mod-qualified `Impl` node (#1992, #2003)
- **C# resolution & memory** — global-namespace `typeBindings` O(files²) OOM eliminated (#1871, #1954) and namespace-siblings OOM with worker-path re-parse removed (#1905); qualified/alias constructor names, `:base`/`:this` initializers, and generic type-arg stripping (#2046); primary-base receiver type normalization (#2036); spurious `IMPORTS` edges from ungated `using` resolution stopped (#1881, #1908)
- **C++ dependent-base and member lookup** — resolution across nested/inline namespaces (#1634, #1814), base-specifier qualifier threading (#1815, #1819), call-site types threaded into qualified member lookup (#1632, #1810), variadic pack dependent lookup (#1909), uninitialized multi-declarators (#1965), and typedef-enum / anonymous-struct declarations (#1941)
- **Kotlin type resolution** — smart-cast refinement for `when/is` and `if/is` (#1758, #1774), overload target-id by parameter types (#1761, #1777), cross-file iterable return propagation (#1759, #1775), method-chain fixpoint receiver types (#1760, #1776), virtual dispatch via constructor type override (#1762, #1778), interface default-method dispatch via implements-split MRO (#1763, #1779), and default-parameter arity detection (#2034)
- **Go declarations** — multi-name declaration capture (#2032), fixed-array parameter binding normalization (#1988), and generic composite-literal constructor inference F33 (#1976)
- **Rust / PHP / Vue / Java parsing** — Rust `struct_expression` name pattern split (#2051); PHP import decomposition, namespace-less `.phtml` module scopes, and Blade-template exclusion (#1801, #1790, #1989); Vue JSDoc, dual-script merge, and lang plumbing F89/F90/F92 (#1936, #2050); Java inherited `RequestMapping` prefix deduplication (#2057) and same-module type resolution for duplicate FQNs (#1712)
- **TypeScript** — HOC pattern false positives fixed with `export default` HOC support (#1943) and suffix-index reuse in the scope resolver (#1840)
- **Inheritance on the worker path** — all languages' inheritance migrated to scope-resolution in worker mode (#1951, #1956); centralized heritage supertype matching (#1921, #1922, #1940); `File->Member` `DEFINES` edges skipped for class members (#1949); phantom `Function` defs for array-method callbacks no longer emitted (#1906)
- **MCP** — sibling-clone repo-ID collisions prevented and generated MCP tool names corrected (#2067); orphan processes avoided by handling stdin close/end and the startup race (#2049); duplicate-name repo resolution disambiguated for worktrees (#1753); Windows setup fallback when global `gitnexus` resolves to a non-spawnable shim (#1694)
- **Worker pool** — resilient zero-copy ingestion worker pool prevents analyze hangs on TS-root-scale loads (#1693); cache-hit native workers no longer abort (#1751, #1833); worker-pool docs drift corrected and worker-side stack surfaced on crash (#2068, #2070)
- **LadybugDB** — FTS loaded in the Windows read pool (#2040) and probed-then-loaded on Windows (#1690, #1692); non-ASCII KuzuDB paths resolved on Windows (#1811, #1817); WAL corruption detected in schema init with recovery surfaced (#1647, #1650); WAL checkpoint-threshold control (#1772); init lock skipped for read-only opens (#1783, #1784); `serve` kept stable when sidecars are missing (#1747)
- **Server / API** — `gitnexus serve` startup restored under Express 5 (#1749); `/api/graph`, `/api/search`, `/api/grep` opened read-only (#1686); native read-only enforcement and prepared statements for Cypher query paths (#1655); `eval-server` localhost binding left to the OS (#1722)
- **Embeddings** — local ONNX runtime guarded on macOS Intel before the transformers.js import (#1987)
- **Web agent** — Nexus AI agent system prompt aligned with registered tools (#1984) and the agent stopped cleanly on user Stop (#1820)
- **Group / contracts** — HTTP graph and source contracts unioned (#1709); `httpx` `AsyncClient` alias imports detected (#1687); Node gRPC `loadPackageDefinition` gate no longer matches every member call (#1916); manifest/workspace extraction moved before `closeLbug` (#1802, #1807)
- **Hooks / install** — `gitnexus` resolved on `PATH` via a pure-Node, all-OS scan (#1938, #1980); offline-first extension installs (#1161); actionable error and docs for the `pnpm dlx`/`pnpx` native-load crash (#307, #1967); `onnxruntime-common` declared as a runtime dependency (#2074); vendored grammars materialized to fix Windows EPERM (#1728, #1729)
- **CLI** — missing LadybugDB native binary detected at startup with actionable guidance (#835, #1837); `--no-stats` applied to the keep-marker stats line (#1706, #1765); skipped large-file paths surfaced by default (#1659, #1661); build.js skipped when running outside the monorepo (#1795, #1816); auto-heap raised to 16 GB with tightened cross-platform OOM guidance for UE5-scale repos (#1652)
- **Wiki** — hidden 60s default timeout removed with timeout/retry flag validation and surfaced timeout errors (#1651); budget-aware grouping to prevent context overflow on large repos (#627, #1832)
- **`detect-changes`** — `resolveWorktreeCwd` guarded against overriding a separately-indexed worktree (#1691)
- **Windows reliability** — `windowsHide:true` passed to every `child_process` spawn-family call (#1794)
### Changed
- **Legacy resolution deletion (Ring 4)** — removed the legacy call-resolution DAG + heritage processor (RING4-1, #942, #2023), the legacy resolution-context + tiered-lookup plumbing (RING4-2, #943, #2033), and the shadow-mode parity harness (RING4-3, #944, #2071)
- **CONTRIBUTING** — clarified local development setup (#2024)
- **Tests / CI** — cli-e2e made read-only and eval-server tests hardened under load (#2000, #1786, #1838, #1688); parity shards consolidated and the cross-platform matrix narrowed (#1798); devcontainer smoke build hardened against Docker Hub flakes (#1969); gitleaks stabilized (#2027)
### Performance
- **Linux-kernel-scale analysis overhaul** — worker-pool parse, finalize O(n²), and the scope-resolution memory wall (#1983, #2038)
- **Scope-capture linearized across all languages (O(n²)→O(n))** plus Python import-resolution linearization (#1918), the Go-specific re-walk fix (#1848, #1915), and owner-keyed lookup for Step 2 member resolution (#1657)
- **C++ ADL candidates indexed once instead of per-site rescans** (#1990)
- **Inert local value symbols pruned** during ingestion (#2065)
### Chore / Dependencies
- `@ladybugdb/core` bump in /gitnexus (#2056)
- Routine dependency bumps across /gitnexus, /gitnexus-web, /eval, and GitHub Actions — incl. `hono`, `vitest`, `@vitest/coverage-v8`, `tsx`, `lru-cache`, `express`/`@types/express`, `express-rate-limit`, `qs`, `node-addon-api`, `brace-expansion`, `langchain`, `i18next`, `dompurify`, `lucide-react`, `axios`, `zod`, `@langchain/langgraph`, `@vercel/node`, `langsmith`, `aiohttp`, `idna`, and the `docker/*` / `github/codeql-action` / `release-drafter` / `dependency-review-action` actions (#2056, #2044, #2043, #2042, #2016, #2015, #2013, #2012, #2011, #2010, #2009, #2008, #2018, #2019, #2017, #2020, #1986, #1911, #1864, #1863, #1861, #1860, #1866, #1844, #1845, #1826, #1825, #1824, #1791, #1789, #1768, #1767, #1739, #1740, #1738, #1736, #1735, #1734, #1731, #1713, #1698, #1697, #1696, #1689, #1604, #1552, #1464, #872)
- **Security** — `@vercel/node` upgraded in /gitnexus-web with transitive advisories remediated (#1705)
## [1.6.5] - 2026-05-16
### Added
+11 -1
View File
@@ -400,7 +400,7 @@ Values above **32768 KB (32 MB)** are clamped to the tree-sitter parser ceiling;
### Analyze reports a worker timeout
Worker parse timeouts are recoverable. GitNexus retries stalled worker jobs with backoff, splits large jobs to isolate slow files, and falls back to the sequential parser when needed. If a large repository needs more time per worker job, use either:
Worker parse timeouts are recoverable. GitNexus retries stalled worker jobs with backoff, splits large jobs to isolate slow files, and quarantines a file that repeatedly crashes its worker (respawning the slot so the pool keeps going). If a large repository needs more time per worker job, use either:
```bash
# CLI flag, in seconds
@@ -423,6 +423,16 @@ Three env vars expose the pool's resilience layers (respawn budget, cumulative-t
| `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits. |
| `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool. |
### Graph cleanup tuning
After scope resolution, analyze prunes inert block-local value symbols (a function-local `const`/`let`/`var` that ends up with only its structural `File→DEFINES` edge) to keep the graph focused on cross-symbol relationships. Module/file-scope symbols, class members, and any local with a real edge are always kept.
| Variable | Default | Effect |
| ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------- |
| `GITNEXUS_KEEP_LOCAL_VALUE_SYMBOLS` | unset | Set to `1`/`true` to keep inert block-local value symbols instead of pruning them. |
Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
## Privacy
- All processing happens locally on your machine
+27 -16
View File
@@ -70,10 +70,17 @@ this doc, run it under instrumentation:
```bash
# From the gitnexus/ subdir:
cd gitnexus
# Single-threaded baseline (sequential fallback):
npx vitest run test/integration/parse-impl-large-fixture.test.ts --reporter=verbose
# The worker pool is the sole parse path, so every run needs the dist worker
# (`npm run build`) and a pool size pinned via GITNEXUS_WORKER_POOL_SIZE.
# Worker-pool path (requires built dist/ — pre-built by `npm run build`):
# Single-worker-pool baseline (closest analog to the old single-threaded run —
# sequential parsing was removed, so a 1-worker pool is the floor):
npm run build && \
GITNEXUS_WORKER_POOL_SIZE=1 \
GITNEXUS_VERBOSE=1 \
npx vitest run test/integration/parse-impl-large-fixture.test.ts --reporter=verbose
# Multi-worker path:
npm run build && \
GITNEXUS_WORKER_POOL_SIZE=4 \
GITNEXUS_PARSE_CHUNK_CONCURRENCY=2 \
@@ -97,22 +104,26 @@ node --inspect=0 \
## Latest measurement
> _No measurement data has been collected yet — this file is the
> methodology + harness scaffold. The single recorded data point is the
> U6 wall-clock smoke baseline below; the worker-pool rows are
> placeholders for future bench-pass output._
> methodology + harness scaffold. The U6 smoke test confirms the
> worker-pool path stays well within its wall-clock budget, but every
> throughput/heap cell below is a `_TBD_` placeholder for a future
> bench-pass._
The U6 integration test (`gitnexus/test/integration/parse-impl-large-fixture.test.ts`)
was observed completing the synthetic fixture in **~6 seconds** under
the sequential path (`skipWorkers: true`) on the development machine,
well under the 30 s `Promise.race` wall-clock budget. That number is a
smoke baseline only — recorded here for reference, not as a regression
target.
runs the worker pool — the sole parse path now that sequential parsing
has been removed (disabling the pool on a repo with parseable files
raises a hard `WorkerPoolDisabledError`). It completes the synthetic
fixture well within the 30 s `Promise.race` wall-clock budget on the
development machine, but no worker-pool throughput/heap numbers have been
captured yet, so the rows below are all `_TBD_`. (An earlier ~6 s figure
recorded here was measured on the now-removed sequential path; it has
been dropped rather than relabelled as a worker-pool baseline, since the
two paths are not comparable.)
| Path | files/s | wall-clock | peak heap | chunks | quarantined |
| ------------------------------------------ | ------- | -------------------- | --------- | ------ | ----------- |
| Sequential (`skipWorkers: true`, U6 smoke) | _TBD_ | ~6 s _(observation)_ | _TBD_ | 17 | 0 |
| Worker pool, `--workers 4`, concurrency 2 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
| Worker pool, `--workers 1`, concurrency 1 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
| Path | files/s | wall-clock | peak heap | chunks | quarantined |
| ------------------------------------------------------------------------- | ------- | ---------- | --------- | ------ | ----------- |
| Worker pool, `--workers 1` (`GITNEXUS_WORKER_POOL_SIZE=1`), concurrency 1 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
| Worker pool, `--workers 4`, concurrency 2 | _TBD_ | _TBD_ | _TBD_ | _TBD_ | 0 |
**Hardware:** _TBD — record OS, CPU, RAM, Node version, gitnexus SHA at
the time of the bench-pass that populates the table above._
@@ -1 +1 @@
06687dff942d531c4d453b5906a8666c90db4867eb43ed18304aa59a8a93ef9d
c03f87cd8cd1ee716dea93cceb27109ee5b4584bc9fe426eea3f18fd6f9854cc
+35 -30
View File
@@ -1,9 +1,9 @@
{
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).",
"go": {
"fingerprint": "a909c197b07921f974de1a8a47cc997b9580153db2d400ef13d205b6c1de5865",
"fingerprint": "09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1",
"scaling_budget": 1.5,
"_rebaselined": "#1966: Go structural interface implementation detection changes capture output (method_elem, return-type, pointer receiver raw form, struct_type/interface_type containers)."
"_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected."
},
"cobol": {
"fingerprint": "68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1",
@@ -11,71 +11,76 @@
"_note": "Updated for F17-F23 fixes (P2: TIMES guard, ADD GIVING, SQL AS alias). See PR #1959."
},
"c": {
"fingerprint": "0de009bdbfe095f530fa87eb32bce6ab83092c904f26b3c8fe8d8ab587cf6dc9",
"fingerprint": "12a196b2d6249c8d86a931b12ecebc2a0cdf8d6f47683acdd0d8e9d8bc7657f5",
"scaling_budget": 1.5,
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance \u2014 flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96."
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance — flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96.",
"_note": "#1983: + c-static-linkage-worker fixture (caller.c/lib.c/lib.h/local.c — worker-path static-linkage side-channel test). 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). The baseline was missed when the fixture landed; regenerated here. fingerprint 0de009b->39f3a83.",
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
},
"cpp": {
"fingerprint": "2f517381e8b03db221d13f65d5e485d021c17a2f7fa684446e09f33d78c3adb9",
"fingerprint": "fd3d3768cdebbb4767d7cf18b8d2df19d61de969c816d7f4d6b599f947811356",
"scaling_budget": 1.5,
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12.",
"_rebaselined": "#1965 / #1923 F4: uninitialized non-leading multi-declarators now emit @declaration.variable captures; cpp-adl-inner-callable-outer-noncallable data::Pair a, b adds the legitimate fixture drift. Linear (~1.06).",
"_note": "#1975: + cpp-out-of-line-class fixture (out-of-line struct Outer::Inner / Other::Inner). Pure fixture-corpus drift — the fix is the legacy structure-query qualified_identifier arm, NOT the cpp scope-extractor; existing fixtures' captures byte-identical. fixture_count 263->265. #1982: cpp-nested-tail-collision gains qualified heritage (struct DerivedA : Outer::Inner, struct DerivedB : Other::Inner) AND emitCppInheritanceCaptures now emits an ADDITIVE @reference.qualified-name capture on QUALIFIED bases (drives the qualified-first inheritance resolver). Purely additive — existing captures unchanged; same fixture file, no new fixture."
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression).",
"_note": "#1975: + cpp-out-of-line-class fixture, fixture_count 263->265. #1990: + cpp-adl-ns-plus-hidden-friend-same-name fixture (ADL hidden-friend + namespace-callable merge parity test). Pure fixture-corpus drift — no scope-extractor change; existing fixtures' captures byte-identical. fixture_count 265->267. #1995: + cpp-union-nested-tail-collision and cpp-anon-ns-tail-collision fixtures — pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture — pure fixture-corpus drift; fixture_count 272->273, fingerprint d63ded6->6d6207ae."
},
"csharp": {
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.)",
"fingerprint": "68ef32c126d5c6de5d8184c6ad0a6104043036daf9805947db8b21741b883f43",
"scaling_budget": 1.5
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1924 F16: record primary-constructor base bindings now exclude constructor arguments; capture fingerprint changes, scaling remains linear. | #2036 review follow-up: csharp-record-base now exercises primary-constructor base dispatch end to end; +2 capture groups, scaling remains linear.",
"fingerprint": "2bb5bc8c19cb8eb08c9590545ad8a1968a7152951f7e12746e2d7901d542fed9",
"scaling_budget": 1.5,
"_note": "#2046: F35 qualified-constructor captures now emit @reference.qualified-name + a simple-name @reference.name on `new Ns.Foo()`/`new A.B.Foo()`; namespace_declaration/file_scoped_namespace_declaration now emit @declaration.namespace name captures (feeding the non-destructive namespacePrefix sidecar for `new B.Foo()` same-tail disambiguation). + csharp-interface-only-base and csharp-namespace-qualified-ctor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.11)."
},
"rust": {
"fingerprint": "30224e2590064745548bc1d623811ae5d37227618854788695442a0acf1898fb",
"fingerprint": "ac610bbe97666bf285923479dd7b43a2fe4c5354aae8df1bcbafdc04fb220f82",
"scaling_budget": 1.5,
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04).",
"_note": "#1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls). Pure fixture-corpus drift — the fix is the legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target, NOT the rust scope-extractor; existing fixtures' captures byte-identical. fixture_count 120->121."
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical. | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126). #1992: + rust-nested-tail-collision-generic and rust-generic-impl-same-method-name (F3) fixtures — pure fixture-corpus drift, no scope-extractor change; fixture_count 127->129, fingerprint 56ffc1c0->b00aea0f."
},
"php": {
"fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f",
"fingerprint": "bc2c27c5ba26d5aea61142a2a99fb772222f5b969205260eb7a71b4c0bd73cdb",
"scaling_budget": 1.5,
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04)."
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04).",
"_note": "PR #1931: F53 import multi-clause, F54 enum_case, F55 anonymous_class — fixture count 138→140, fingerprint drift expected."
},
"ruby": {
"fingerprint": "011c0533318ff61d99267d501866af8a616b1c212050628339064d8c149aafbf",
"fingerprint": "b5ea93bb3d0469c3821a8c70f5d5991c6f326e41097c119ad691154301dcc753",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.)",
"_note": "F62: + scope_resolution class/module declaration captures — fixture count 78→81, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) — pure fixture-corpus drift, scope-extractor captures unchanged; 81→82. #1978: + ruby-nested-tail-collision (same-tail Inner under Outer/Other modules + Shapes.Circle attr_accessor) — pure fixture-corpus drift, scope-extractor captures unchanged; 82→83. #1982: that fixture gains same-tail include + attr_accessor, AND emitRubyScopeCaptures now emits the FULL qualified owner in __heritage__/__property__ markers (buildEnclosingQualifiedName) — a CODE change LOCALIZED to nested owners: the golden shows ONLY this fixture drifts (+10 capture groups), all other ruby fixtures byte-identical, and 142/142 resolver tests pass. fixture_count 83 (same file)."
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "F62: + scope_resolution class/module declaration captures — fixture count 78→81, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) — pure fixture-corpus drift, scope-extractor captures unchanged; 81→82. #1991: + ruby-nested-mixin-tail-collision fixture (85→86). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb."
},
"swift": {
"fingerprint": "53325c6345161c5a495f997297af5a24fb718fd3e6647040160f8ab2a2c8e4c0",
"fingerprint": "180ac68e780bdf6f9089d53f51cbb9a66aed3e7774631cc3fcbaae5020213998",
"scaling_budget": 1.5,
"_rebaselined": "#1956: swift-qualified-base fixture + heritage-bearing scale source (class: Base, Serviceable \u2014 extends + protocol conformance); linear (~1.03)."
"_rebaselined": "#1919 open-language coverage: new lang-resolution fixtures + intended capture additions (F5/F9 c-cpp, F26/F28/F29 dart, F47/F48/F49/F51/F52 kotlin, F75/F79 swift). Fingerprint-only drift; scaling_ratio ~1.0 (linear, no perf regression)."
},
"dart": {
"fingerprint": "a9e882b537765e8fd0ddfcd33b38b253dd86fc5ddffa6e4bf5a85ed8ee615eaa",
"fingerprint": "94bf2c26e1ba96f4211634aa572c0a989b503e717e75dfc5df04f66c417de80f",
"scaling_budget": 1.5,
"_added": "#939: dart added to the scope-capture bench with the registry-primary migration. Heritage-bearing scale source (Entity extends Base implements Marker) gates the @reference.inherits synth + the postfix-chain reference walk at scale. emitDartScopeCaptures threads tree-sitter captured nodes (no findNodeAtRange root-walk), so it is linear (~1.0).",
"_rebaselined": "#1970 review + tri-review follow-ups: constructor-call retag, cascade calls, built-in suppression, enum scope, #1926 F24/F25, named-ctor dedup (crash fix), container-name binding suppression; heritage file-affinity resolution. Fixtures: member-call-contexts, constructor-body, named-constructor-body, heritage-name-collision, construct-cascade."
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0."
},
"java": {
"fingerprint": "b63f9be458f7ece854e7b007159d7bf65b4b66a86e83a6c0656fc93ebd5d83da",
"fingerprint": "9b29cafe32873b4902bda311bd089ffc04efe08f13557b966d29544be514080a",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.)"
"_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged.",
"_note": "#1928 / #2045: F35 adds qualified + qualified-generic constructor query captures (`new pkg.Foo()`, `new a.b.Foo()`, `new pkg.Box<T>()`); F38 synthesizes `@reference.call.constructor` on `super(...)`/`this(...)` explicit_constructor_invocation nodes; F41 generic-aware stripQualifier in interpret (type-binding normalization). + java-qualified-constructor and java-explicit-constructor fixtures. Pure capture-additive + fixture-corpus drift; scaling stays linear (~1.06)."
},
"typescript": {
"fingerprint": "3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9",
"scaling_budget": 1.5,
"_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures \u2014 fingerprint drift expected.",
"_note": "#1968: F44, F85, F87 \u2014 fingerprint drift expected."
"_rebaselined": "#1962: F44 (class scope@), F85 (enum member declarations), F87 (optional_parameter type annotations) add new captures — fingerprint drift expected.",
"_note": "#1968: F44, F85, F87 — fingerprint drift expected."
},
"javascript": {
"fingerprint": "a8ddfb15620ae55e50651fc21ab14c4a1f874d9b19e208cc6cbf0a8daac8ec5b",
"fingerprint": "d72f03c6c502235d2d4b74d66baa5c7d361f040d7a1b72e84acad61210d05ae8",
"scaling_budget": 1.5,
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (extends Base); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05)."
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged."
},
"kotlin": {
"fingerprint": "5121a11855cd9cc44a357ae3ff50953de80cdd743f00e8924c31503b132bcd84",
"fingerprint": "90aa832978d9744e50058e77a04748390a7e34e36b309f6c1d178eb07280b7ea",
"scaling_budget": 1.5,
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (: Base()); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
"_rebaselined": "#1956 synth-widening: + kotlin-qualified-base fixture; synthesizeKotlinInheritanceReferences now handles the explicit_delegation form (class F : Iface by d -> Iface), matching the #1940 legacy leg, at parity. Linear (~0.87)."
"_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0."
}
}
+70 -42
View File
@@ -27,6 +27,8 @@
*/
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const NPX_REF = 'gitnexus@latest';
@@ -34,46 +36,72 @@ const NPX_REF = 'gitnexus@latest';
const PNPM_ALLOW_BUILD_BASE = ['@ladybugdb/core', 'gitnexus', 'tree-sitter'];
const PNPM_ALLOW_BUILD_EMBEDDINGS = ['onnxruntime-node'];
// Probe timeout, kept under Claude Code's 10s hook budget. 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 four 1s probes
// (which gitnexus, npm --version, which pnpm, pnpm --version), so the worst case
// is ~9s — within budget but tight. A healthy `which`/`where`/`--version`
// returns in well under a second, so the realistic cost is far lower.
// 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;
/**
* Pick the best match from `where`/`which` output. A global `gitnexus` may be a
* `.cmd`/`.bat` (npm), a `.exe`, or an extensionless shim (Volta, scoop), so on
* Windows we prefer a recognized executable extension but accept any hit — the
* emitted hint is `gitnexus analyze` regardless of which shim resolves it. Pure
* and exported so the shim-matching can be unit-tested without spawning.
* 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 pickPathMatch(output, { isWin, gitnexusWrapper } = {}) {
const lines = output
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
if (isWin && gitnexusWrapper) {
return lines.find((l) => /\.(cmd|bat|exe)$/i.test(l)) || lines[0] || null;
}
return lines[0] || null;
}
/** Absolute path to `command` on PATH, or null. `gitnexusWrapper` enables the Windows shim match. */
function resolveOnPath(command, gitnexusWrapper = false) {
const isWin = process.platform === 'win32';
try {
const output = execFileSync(isWin ? 'where' : 'which', [command], {
encoding: 'utf-8',
timeout: PROBE_TIMEOUT_MS,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
});
return pickPathMatch(output, { isWin, gitnexusWrapper });
} catch {
return null;
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
@@ -193,12 +221,12 @@ function formatPnpmDlxCommand(gitnexusArgs, options = {}, deps = {}) {
function formatAnalyzeCommand(options = {}, deps = {}) {
const suffix = options.embeddings ? ' --embeddings' : '';
// Keep the stale-index hook budget tight by querying each tool at most once.
// A memoized PATH probe is shared with resolveInvocationMode (so `gitnexus`
// isn't probed twice), and pnpm's version is captured by a single
// `pnpm --version` that proves both presence (for mode resolution) and
// version (for the allow-build gate) — replacing the former `which pnpm` +
// `pnpm --version` double spawn. Injected deps (tests) and forced/global
// modes skip the pnpm probe.
// 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}`;
@@ -256,7 +284,7 @@ module.exports = {
formatPnpmDlxCommand,
resolveInvocationMode,
buildRunnerArgv,
pickPathMatch,
resolveOnPath,
getNpmMajorVersion,
NPX_REF,
PNPM_ALLOW_BUILD_BASE,
+360 -167
View File
@@ -1,17 +1,17 @@
{
"name": "gitnexus",
"version": "1.6.5",
"version": "1.6.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitnexus",
"version": "1.6.5",
"version": "1.6.6",
"hasInstallScript": true,
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
"@huggingface/transformers": "^4.1.0",
"@ladybugdb/core": "^0.16.1",
"@ladybugdb/core": "^0.17.0",
"@modelcontextprotocol/sdk": "^1.0.0",
"@scarf/scarf": "^1.4.0",
"cli-progress": "^3.12.0",
@@ -26,14 +26,13 @@
"ignore": "^7.0.5",
"js-yaml": "^4.1.1",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.0.0",
"mnemonist": "^0.40.3",
"node-addon-api": "8.8.0",
"onnxruntime-common": "^1.26.0",
"onnxruntime-node": "^1.24.0",
"pandemonium": "^2.4.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"tree-sitter": "^0.21.1",
"tree-sitter": "0.21.1",
"tree-sitter-c": "0.21.4",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.2",
@@ -1160,27 +1159,28 @@
}
},
"node_modules/@ladybugdb/core": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.16.1.tgz",
"integrity": "sha512-qwuEcR8CVMKb6tNDaHtq7Ux8hT/XbPC0db+vwutX6JxNAejyx7YomHKPSy9XAKURhYK8mezZe3UN8rf+xpHOjQ==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ladybugdb/core/-/core-0.17.0.tgz",
"integrity": "sha512-fg7EGEJUj6H5JJLpU3iD5R0pAEc9u2OkU7ufX10krph9bCIhQyt/Tk6y/g4+x9zi/IHXegOjAVfSpWZp1gpoAA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"apache-arrow": "^21.1.0",
"cmake-js": "^8.0.0",
"node-addon-api": "^6.0.0"
},
"optionalDependencies": {
"@ladybugdb/core-darwin-arm64": "0.16.1",
"@ladybugdb/core-darwin-x64": "0.16.1",
"@ladybugdb/core-linux-arm64": "0.16.1",
"@ladybugdb/core-linux-x64": "0.16.1",
"@ladybugdb/core-win32-x64": "0.16.1"
"@ladybugdb/core-darwin-arm64": "0.17.0",
"@ladybugdb/core-darwin-x64": "0.17.0",
"@ladybugdb/core-linux-arm64": "0.17.0",
"@ladybugdb/core-linux-x64": "0.17.0",
"@ladybugdb/core-win32-x64": "0.17.0"
}
},
"node_modules/@ladybugdb/core-darwin-arm64": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.16.1.tgz",
"integrity": "sha512-Nl+Cf70rD+HaC9IBHv+oeUwqX9plghXD7PN9tyMzMohRVPvcGEbqWPB6YcdJa8rR7qRqCCbmaNMDen5wg4rY2w==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-arm64/-/core-darwin-arm64-0.17.0.tgz",
"integrity": "sha512-wghUBEmcQ9U10QOyOxXVQTZ6SHtkB8QV3sJHwCj2Dn5B/SsaB36kA4l2oKbgXpKDgjmSYiz3DfMs5yfDqen9UA==",
"cpu": [
"arm64"
],
@@ -1191,9 +1191,9 @@
]
},
"node_modules/@ladybugdb/core-darwin-x64": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.16.1.tgz",
"integrity": "sha512-4eAjfimAAQRSmDfUUkGrl9OhefxcW1ziA9tl0eljBlGoUseE7dL02+RSqjGohYMcQ+lzuHAq1QWb0XRlMA8YTQ==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-darwin-x64/-/core-darwin-x64-0.17.0.tgz",
"integrity": "sha512-f0QRhmDY8NEMjAT3IbFELMEFxAnKq6trppn1vgAFIk9wTD/MKakfX/gtXOazpOZQHkwlfNgs+WSkJFfFvQ4JaQ==",
"cpu": [
"x64"
],
@@ -1204,9 +1204,9 @@
]
},
"node_modules/@ladybugdb/core-linux-arm64": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.16.1.tgz",
"integrity": "sha512-zkctksev+hsPFrNxHHdq4lYK5OWdLhWfRdQzjzkgDyaHayHU6yCL2fgD6uPGQ8TRQ6/2DxMErb4p3FzGW85Ubw==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-arm64/-/core-linux-arm64-0.17.0.tgz",
"integrity": "sha512-TS9nbkvLJZt3Tgm6zzd/QuZmTiVoyQTPfbbwPej0VfcCVAfa1sDpZtoMlwse9EVHOrowQ0SorJVPg194lYVxdg==",
"cpu": [
"arm64"
],
@@ -1217,9 +1217,9 @@
]
},
"node_modules/@ladybugdb/core-linux-x64": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.16.1.tgz",
"integrity": "sha512-5rAb9T5vif8WKhHwhobosu2/aiOwJkWb/ViybvUc5GFKunKl8VI6RmZQVeufT9zUzRktUwrxBrxblCxsnamXJw==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-linux-x64/-/core-linux-x64-0.17.0.tgz",
"integrity": "sha512-T/C0QKDoBCs8s/NQ2Udip8lZgJ8MzLqs2rRgreDd2dCP3aNnXu8eOBe10RSoRO5PiZZIZihXoz4Q8KMM6FtBGQ==",
"cpu": [
"x64"
],
@@ -1230,9 +1230,9 @@
]
},
"node_modules/@ladybugdb/core-win32-x64": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.16.1.tgz",
"integrity": "sha512-ShOUTrIuZKQ63J95tcRJxKf1cvg8yi2FSYx9kMTSercc1FdQZPV+zxUN0myMq3MTWOl7xDxsVMmdp/t80O29UQ==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ladybugdb/core-win32-x64/-/core-win32-x64-0.17.0.tgz",
"integrity": "sha512-XrQrbPD3h+MhP94jVu+4VgNnp8LKSskPll+/au+Ug3yqpXZ0We9lXX5+rW4NHuIYdAYy4iLheYz4OuozUS20qg==",
"cpu": [
"x64"
],
@@ -1308,9 +1308,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.132.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
"integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1388,9 +1388,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
"integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -1405,9 +1405,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
"integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -1422,9 +1422,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
"integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -1439,9 +1439,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
"integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -1456,9 +1456,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
"integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -1473,9 +1473,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
"integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
@@ -1490,9 +1490,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
"integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
@@ -1507,9 +1507,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
"integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
@@ -1524,9 +1524,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
"integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
@@ -1541,9 +1541,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
"integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
@@ -1558,9 +1558,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
"integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
@@ -1575,9 +1575,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
"integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -1592,9 +1592,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
"integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -1622,9 +1622,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
"integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -1639,9 +1639,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
"integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -1676,6 +1676,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -1719,6 +1728,18 @@
"@types/node": "*"
}
},
"node_modules/@types/command-line-args": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
"integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
"license": "MIT"
},
"node_modules/@types/command-line-usage": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
"integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
"license": "MIT"
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@@ -1848,14 +1869,14 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz",
"integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz",
"integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.7",
"@vitest/utils": "4.1.8",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -1869,8 +1890,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "4.1.7",
"vitest": "4.1.7"
"@vitest/browser": "4.1.8",
"vitest": "4.1.8"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1879,16 +1900,16 @@
}
},
"node_modules/@vitest/expect": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
"integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.7",
"@vitest/utils": "4.1.7",
"@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.8",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -1897,13 +1918,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz",
"integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.7",
"@vitest/spy": "4.1.8",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -1924,9 +1945,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz",
"integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1937,13 +1958,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz",
"integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.7",
"@vitest/utils": "4.1.8",
"pathe": "^2.0.3"
},
"funding": {
@@ -1951,14 +1972,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz",
"integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.7",
"@vitest/utils": "4.1.7",
"@vitest/pretty-format": "4.1.8",
"@vitest/utils": "4.1.8",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -1967,9 +1988,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz",
"integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1977,13 +1998,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz",
"integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.7",
"@vitest/pretty-format": "4.1.8",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -2070,12 +2091,56 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/apache-arrow": {
"version": "21.1.0",
"resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.1.0.tgz",
"integrity": "sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.11",
"@types/command-line-args": "^5.2.3",
"@types/command-line-usage": "^5.0.4",
"@types/node": "^24.0.3",
"command-line-args": "^6.0.1",
"command-line-usage": "^7.0.1",
"flatbuffers": "^25.1.24",
"json-bignum": "^0.0.3",
"tslib": "^2.6.2"
},
"bin": {
"arrow2csv": "bin/arrow2csv.js"
}
},
"node_modules/apache-arrow/node_modules/@types/node": {
"version": "24.13.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.0.tgz",
"integrity": "sha512-5vtOqGQr4NJKeEzV441FcOi2MeG9UTWq9LqVLGneDdu4vlX17H8kQ2PA2UmNwCUGPVDj4oBjNhS7ReVEIWJJrg==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/apache-arrow/node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/array-back": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz",
"integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -2200,6 +2265,37 @@
"node": ">=18"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2282,6 +2378,44 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"license": "MIT"
},
"node_modules/command-line-args": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.2.tgz",
"integrity": "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.3",
"find-replace": "^5.0.2",
"lodash.camelcase": "^4.3.0",
"typical": "^7.3.0"
},
"engines": {
"node": ">=12.20"
},
"peerDependencies": {
"@75lb/nature": "latest"
},
"peerDependenciesMeta": {
"@75lb/nature": {
"optional": true
}
}
},
"node_modules/command-line-usage": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz",
"integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"chalk-template": "^0.4.0",
"table-layout": "^4.1.1",
"typical": "^7.3.0"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
@@ -2820,6 +2954,23 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/find-replace": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz",
"integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@75lb/nature": "latest"
},
"peerDependenciesMeta": {
"@75lb/nature": {
"optional": true
}
}
},
"node_modules/flatbuffers": {
"version": "25.9.23",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
@@ -3058,7 +3209,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -3107,9 +3257,9 @@
"license": "MIT"
},
"node_modules/hono": {
"version": "4.12.18",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz",
"integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==",
"version": "4.12.23",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz",
"integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -3297,6 +3447,14 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-bignum": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
"integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -3588,6 +3746,12 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -3595,9 +3759,9 @@
"license": "Apache-2.0"
},
"node_modules/lru-cache": {
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
"integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
@@ -4292,13 +4456,13 @@
}
},
"node_modules/rolldown": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
"integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.132.0",
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -4308,21 +4472,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.2",
"@rolldown/binding-darwin-arm64": "1.0.2",
"@rolldown/binding-darwin-x64": "1.0.2",
"@rolldown/binding-freebsd-x64": "1.0.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
"@rolldown/binding-linux-arm64-gnu": "1.0.2",
"@rolldown/binding-linux-arm64-musl": "1.0.2",
"@rolldown/binding-linux-ppc64-gnu": "1.0.2",
"@rolldown/binding-linux-s390x-gnu": "1.0.2",
"@rolldown/binding-linux-x64-gnu": "1.0.2",
"@rolldown/binding-linux-x64-musl": "1.0.2",
"@rolldown/binding-openharmony-arm64": "1.0.2",
"@rolldown/binding-wasm32-wasi": "1.0.2",
"@rolldown/binding-win32-arm64-msvc": "1.0.2",
"@rolldown/binding-win32-x64-msvc": "1.0.2"
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/router": {
@@ -4694,7 +4858,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -4703,6 +4866,19 @@
"node": ">=8"
}
},
"node_modules/table-layout": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
"integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
"license": "MIT",
"dependencies": {
"array-back": "^6.2.2",
"wordwrapjs": "^5.1.0"
},
"engines": {
"node": ">=12.17"
}
},
"node_modules/tar": {
"version": "7.5.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
@@ -4749,9 +4925,9 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5036,13 +5212,12 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz",
"integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5115,6 +5290,15 @@
"node": ">=14.17"
}
},
"node_modules/typical": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
"integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
@@ -5168,17 +5352,17 @@
}
},
"node_modules/vite": {
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.15",
"rolldown": "1.0.2",
"tinyglobby": "^0.2.16"
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -5246,19 +5430,19 @@
}
},
"node_modules/vitest": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz",
"integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==",
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.7",
"@vitest/mocker": "4.1.7",
"@vitest/pretty-format": "4.1.7",
"@vitest/runner": "4.1.7",
"@vitest/snapshot": "4.1.7",
"@vitest/spy": "4.1.7",
"@vitest/utils": "4.1.7",
"@vitest/expect": "4.1.8",
"@vitest/mocker": "4.1.8",
"@vitest/pretty-format": "4.1.8",
"@vitest/runner": "4.1.8",
"@vitest/snapshot": "4.1.8",
"@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.8",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -5286,12 +5470,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.7",
"@vitest/browser-preview": "4.1.7",
"@vitest/browser-webdriverio": "4.1.7",
"@vitest/coverage-istanbul": "4.1.7",
"@vitest/coverage-v8": "4.1.7",
"@vitest/ui": "4.1.7",
"@vitest/browser-playwright": "4.1.8",
"@vitest/browser-preview": "4.1.8",
"@vitest/browser-webdriverio": "4.1.8",
"@vitest/coverage-istanbul": "4.1.8",
"@vitest/coverage-v8": "4.1.8",
"@vitest/ui": "4.1.8",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -5367,6 +5551,15 @@
"node": ">=8"
}
},
"node_modules/wordwrapjs": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz",
"integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==",
"license": "MIT",
"engines": {
"node": ">=12.17"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "gitnexus",
"version": "1.6.5",
"version": "1.6.6",
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
"author": "Abhigyan Patwari",
"license": "PolyForm-Noncommercial-1.0.0",
@@ -48,7 +48,6 @@
"test:integration": "vitest run test/integration",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:parity": "tsx scripts/run-parity.ts",
"test:cross-platform": "tsx scripts/run-cross-platform.ts",
"postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs && node scripts/build-tree-sitter-swift.cjs",
"prepare": "node scripts/build.js",
@@ -56,7 +55,7 @@
},
"dependencies": {
"@huggingface/transformers": "^4.1.0",
"@ladybugdb/core": "^0.16.1",
"@ladybugdb/core": "^0.17.0",
"@modelcontextprotocol/sdk": "^1.0.0",
"@scarf/scarf": "^1.4.0",
"cli-progress": "^3.12.0",
@@ -71,8 +70,8 @@
"ignore": "^7.0.5",
"js-yaml": "^4.1.1",
"jsonc-parser": "^3.3.1",
"lru-cache": "^11.0.0",
"mnemonist": "^0.40.3",
"onnxruntime-common": "^1.26.0",
"onnxruntime-node": "^1.24.0",
"pandemonium": "^2.4.0",
"pino": "^10.3.1",
+1 -3
View File
@@ -4,10 +4,8 @@
* isolating the resolution cost from parse / heritage / pipeline
* overhead.
*
* Usage: REGISTRY_PRIMARY_PYTHON=1 npx tsx scripts/bench-scope-resolution.ts
* Usage: npx tsx scripts/bench-scope-resolution.ts
*/
process.env.REGISTRY_PRIMARY_PYTHON = '1';
import { generateId } from '../src/lib/utils.js';
import { createKnowledgeGraph } from '../src/core/graph/graph.js';
import { runScopeResolution } from '../src/core/ingestion/scope-resolution/index.js';
@@ -1,24 +0,0 @@
/**
* CI helper — emits the `MIGRATED_LANGUAGES` set as a JSON matrix array for
* GitHub Actions (`.github/workflows/ci-scope-parity.yml`).
*
* Consumed by the `discover` job in that workflow. Each entry has:
* - `slug`: lowercase language id, matching `test/integration/resolvers/<slug>.test.ts`.
* - `envvar`: uppercase suffix used to build the `REGISTRY_PRIMARY_<envvar>` toggle.
*
* Run with `npx tsx scripts/ci-list-migrated-languages.ts`. The script
* writes a single JSON array to stdout (no wrapper object) so the
* workflow can pipe it straight into `$GITHUB_OUTPUT`.
*/
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
const entries = [...MIGRATED_LANGUAGES].map((slug) => {
const s = String(slug);
return {
slug: s,
envvar: s.toUpperCase().replace(/-/g, '_'),
};
});
process.stdout.write(JSON.stringify(entries));
+1 -1
View File
@@ -33,7 +33,7 @@ const PLATFORM_LOGIC = [
'test/unit/resolve-invocation.test.ts',
'test/unit/platform-capabilities.test.ts',
'test/unit/worker-pool-windows-quarantine.test.ts',
'test/unit/lbug-pool-win-fts-probe.test.ts',
'test/unit/lbug-pool-fts-load.test.ts',
'test/unit/repo-manager.test.ts',
'test/unit/repo-manager-finalize-invariant.test.ts',
'test/unit/hooks.test.ts',
-128
View File
@@ -1,128 +0,0 @@
/**
* Consolidated scope-resolution parity runner.
*
* Replaces the per-language matrix in ci-scope-parity.yml with a single
* job that runs all migrated languages sequentially in one process. This
* eliminates 8× redundant checkout + npm ci + build cycles (the old
* workflow created a separate GitHub Actions job per language).
*
* For each language in MIGRATED_LANGUAGES:
* 1. Run its resolver test with REGISTRY_PRIMARY_<LANG>=0 (legacy DAG)
* 2. Run its resolver test with REGISTRY_PRIMARY_<LANG>=1 (registry-primary)
*
* Both modes must pass. Failures are collected and reported at the end
* so all regressions are visible in a single CI run (equivalent to the
* old workflow's fail-fast: false behavior).
*
* Vitest output streams to the console in real time (stdio: 'inherit')
* so CI logs show the actual test output directly. No per-invocation
* timeout — the CI job-level timeout (30 min) is the outer guard.
*
* Usage:
* npx tsx scripts/run-parity.ts
* npx tsx scripts/run-parity.ts --language python # single language
*/
import { execFileSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
interface ParityFailure {
lang: string;
mode: 'legacy' | 'registry-primary';
}
function envVarName(slug: string): string {
return `REGISTRY_PRIMARY_${slug.toUpperCase().replace(/-/g, '_')}`;
}
function testFilePath(slug: string): string {
return `test/integration/resolvers/${slug}.test.ts`;
}
function runVitest(testFile: string, env: Record<string, string>): boolean {
try {
execFileSync('npx', ['vitest', 'run', testFile], {
cwd: ROOT,
env: { ...process.env, ...env },
stdio: 'inherit',
shell: true,
});
return true;
} catch {
return false;
}
}
// Parse CLI args
const args = process.argv.slice(2);
const langFlag = args.indexOf('--language');
const singleLang = langFlag >= 0 ? args[langFlag + 1] : undefined;
if (langFlag >= 0 && singleLang === undefined) {
console.error('--language requires a value');
process.exit(1);
}
const languages = singleLang ? [singleLang] : [...MIGRATED_LANGUAGES].map(String);
// Verify test files exist before running
const missingFiles: string[] = [];
for (const lang of languages) {
const file = path.resolve(ROOT, testFilePath(lang));
try {
fs.accessSync(file);
} catch {
missingFiles.push(`${testFilePath(lang)} (${lang})`);
}
}
if (missingFiles.length > 0) {
console.error('Missing resolver test files:');
for (const f of missingFiles) console.error(` ${f}`);
process.exit(1);
}
console.log(`Scope-resolution parity: ${languages.length} language(s)`);
console.log(`Languages: ${languages.join(', ')}\n`);
const failures: ParityFailure[] = [];
for (const lang of languages) {
const file = testFilePath(lang);
const envVar = envVarName(lang);
console.log(`\n── ${lang} — legacy DAG (${envVar}=0) ──`);
if (!runVitest(file, { [envVar]: '0' })) {
failures.push({ lang, mode: 'legacy' });
}
console.log(`\n── ${lang} — registry-primary (${envVar}=1) ──`);
if (!runVitest(file, { [envVar]: '1' })) {
failures.push({ lang, mode: 'registry-primary' });
}
}
// Summary
const total = languages.length * 2;
const passed = total - failures.length;
console.log('\n═══════════════════════════════════════');
console.log('PARITY SUMMARY');
console.log('═══════════════════════════════════════');
console.log(`Passed: ${passed}/${total}`);
if (failures.length > 0) {
console.log(`\nFAILURES (${failures.length}):`);
for (const f of failures) {
console.log(` ✗ ${f.lang} [${f.mode}]`);
}
process.exit(1);
}
console.log('\nAll parity checks passed.');
-291
View File
@@ -1,291 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>GitNexus — Shadow Parity Dashboard</title>
<!--
Static dashboard for the RFC #909 shadow-mode parity report.
Reads `latest.json` from this directory and renders a per-language
parity table. Zero build step, zero runtime dependencies — a
single file that any browser or file:// context can open.
Usage:
# from repo root, after a shadow-mode run
cp .gitnexus/shadow-parity/latest.json gitnexus/shadow-parity-dashboard/
open gitnexus/shadow-parity-dashboard/index.html
CI artifact wiring (follow-up): the CI job publishes a snapshot
of this directory + latest.json as a downloadable bundle per run.
-->
<style>
:root {
color-scheme: light dark;
--fg: #1f2937;
--fg-muted: #6b7280;
--bg: #ffffff;
--bg-muted: #f9fafb;
--border: #e5e7eb;
--good: #16a34a;
--warn: #d97706;
--bad: #dc2626;
--primary-tag-legacy: #7c3aed;
--primary-tag-registry: #0ea5e9;
}
@media (prefers-color-scheme: dark) {
:root {
--fg: #e5e7eb;
--fg-muted: #9ca3af;
--bg: #111827;
--bg-muted: #1f2937;
--border: #374151;
}
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
font:
14px/1.45 system-ui,
-apple-system,
sans-serif;
}
main {
max-width: 1200px;
margin: 0 auto;
padding: 24px 16px;
}
h1 {
font-size: 20px;
margin: 0 0 4px;
}
.meta {
color: var(--fg-muted);
font-size: 12px;
margin-bottom: 20px;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.card {
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
background: var(--bg-muted);
}
.card .k {
color: var(--fg-muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.card .v {
font-size: 20px;
font-weight: 600;
}
table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
}
th,
td {
padding: 6px 10px;
text-align: right;
border-bottom: 1px solid var(--border);
}
th:first-child,
td:first-child {
text-align: left;
}
thead th {
font-weight: 600;
color: var(--fg-muted);
font-size: 12px;
background: var(--bg-muted);
}
tbody tr:hover {
background: var(--bg-muted);
}
.parity {
font-weight: 600;
}
.parity.good {
color: var(--good);
}
.parity.warn {
color: var(--warn);
}
.parity.bad {
color: var(--bad);
}
.tag {
display: inline-block;
padding: 1px 6px;
border-radius: 10px;
font-size: 10px;
margin-left: 6px;
color: white;
}
.tag.legacy {
background: var(--primary-tag-legacy);
}
.tag.registry {
background: var(--primary-tag-registry);
}
.empty {
padding: 40px;
text-align: center;
color: var(--fg-muted);
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: var(--bg-muted);
padding: 1px 4px;
border-radius: 3px;
}
</style>
</head>
<body>
<main>
<h1>Shadow Parity — RFC #909</h1>
<div class="meta" id="meta">loading <code>latest.json</code>…</div>
<div class="cards" id="cards"></div>
<table id="per-language">
<thead>
<tr>
<th>Language</th>
<th>Total</th>
<th>Agree</th>
<th>Only legacy</th>
<th>Only new</th>
<th>Disagree</th>
<th>Both empty</th>
<th>Parity</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div id="empty" class="empty" style="display: none">
No records yet. Enable <code>GITNEXUS_SHADOW_MODE=1</code> and run ingestion to populate.
</div>
</main>
<script>
/* global fetch, document */
(async function () {
const tbody = document.querySelector('#per-language tbody');
const cards = document.getElementById('cards');
const meta = document.getElementById('meta');
const empty = document.getElementById('empty');
const table = document.getElementById('per-language');
let payload;
try {
const r = await fetch('./latest.json', { cache: 'no-store' });
if (!r.ok) throw new Error('HTTP ' + r.status);
payload = await r.json();
} catch (err) {
meta.textContent = 'Failed to load latest.json: ' + err.message;
table.style.display = 'none';
empty.style.display = 'block';
return;
}
const primary = payload.primaryByLanguage || {};
const report = payload.report || {};
const perLang = report.perLanguage || [];
const overall = report.overall || {};
meta.textContent =
'Run ' +
payload.runId +
' — generated ' +
payload.generatedAt +
' (schema v' +
payload.schemaVersion +
')';
// Overall summary cards.
cards.innerHTML = '';
const overallParity = overall.parity !== undefined ? overall.parity : 0;
cards.appendChild(makeCard('Total calls', overall.totalCalls ?? 0));
cards.appendChild(makeCard('Both agree', overall.bothAgree ?? 0));
cards.appendChild(makeCard('Disagree', overall.bothDisagree ?? 0));
cards.appendChild(makeCard('Overall parity', formatPct(overallParity)));
if (!perLang.length) {
table.style.display = 'none';
empty.style.display = 'block';
return;
}
for (const row of perLang) {
const tr = document.createElement('tr');
const primaryTag = primary[row.language];
const tag = primaryTag
? '<span class="tag ' + primaryTag + '">primary: ' + primaryTag + '</span>'
: '';
const parityClass = parityClassFor(row.parity);
tr.innerHTML =
'<td>' +
escape(row.language) +
tag +
'</td>' +
'<td>' +
row.totalCalls +
'</td>' +
'<td>' +
row.bothAgree +
'</td>' +
'<td>' +
row.onlyLegacy +
'</td>' +
'<td>' +
row.onlyNew +
'</td>' +
'<td>' +
row.bothDisagree +
'</td>' +
'<td>' +
row.bothEmpty +
'</td>' +
'<td class="parity ' +
parityClass +
'">' +
formatPct(row.parity) +
'</td>';
tbody.appendChild(tr);
}
function makeCard(k, v) {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML =
'<div class="k">' + escape(k) + '</div><div class="v">' + escape(String(v)) + '</div>';
return div;
}
function formatPct(x) {
if (typeof x !== 'number' || !isFinite(x)) return '—';
return (x * 100).toFixed(1) + '%';
}
function parityClassFor(x) {
if (typeof x !== 'number') return '';
if (x >= 0.95) return 'good';
if (x >= 0.8) return 'warn';
return 'bad';
}
function escape(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
})();
</script>
</body>
</html>
+14 -14
View File
@@ -16,10 +16,10 @@ 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 `node .gitnexus/run.cjs analyze` in terminal.
@@ -28,11 +28,11 @@ description: "Use when the user is debugging a bug, tracing an error, or asking
```
- [ ] 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
+10 -10
View File
@@ -18,8 +18,8 @@ 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
```
@@ -29,9 +29,9 @@ description: "Use when the user asks how code works, wants to understand archite
```
- [ ] 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
+9 -9
View File
@@ -17,9 +17,9 @@ 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
```
@@ -28,11 +28,11 @@ description: "Use when the user wants to know what will break if they change som
## 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 -18
View File
@@ -18,10 +18,10 @@ 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
```
@@ -32,10 +32,10 @@ description: "Use when the user wants to review a pull request, understand what
```
- [ ] 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:
+24 -24
View File
@@ -16,9 +16,9 @@ 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
```
@@ -29,65 +29,65 @@ description: "Use when the user wants to rename, extract, split, move, or restru
### 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
```
+85 -7
View File
@@ -29,6 +29,12 @@ export interface AIContextOptions {
skipAgentsMd?: boolean;
noStats?: boolean;
skipSkills?: boolean;
/**
* Default branch used by the generated regression-compare example (#243).
* Resolved by the CLI (CLI flag > `.gitnexusrc` > auto-detect > "main"); a
* plain caller that omits it gets "main", preserving prior behavior.
*/
defaultBranch?: string;
}
const GITNEXUS_START_MARKER = '<!-- gitnexus:start -->';
@@ -89,6 +95,16 @@ async function findGroupsContainingRegistryName(registryName: string): Promise<s
return hits;
}
/**
* Strip backticks from a branch name before it is embedded in a Markdown
* inline-code span (#1996 tri-review P1). validateBranchName already rejects
* backticks for CLI/config/auto-detect inputs; this is the last-line defense at
* the generation sink so the embedding is provably safe regardless of caller.
*/
export function markdownSafeBranch(branch: string): string {
return branch.replace(/`/g, '');
}
export function generateGitNexusContent(
projectName: string,
stats: RepoStats,
@@ -100,6 +116,14 @@ export function generateGitNexusContent(
// index (#1945). Referenced by docs so a single CLI-neutral command resolves
// the available runner (global `gitnexus` → `pnpm dlx` → `npx`) at call time.
runnerPath: string = '.gitnexus/run.cjs',
// Default branch for the regression-compare example (#243). Configurable so
// projects on `develop`/`master`/etc. don't get `base_ref: "main"` rewritten
// back over their fix on every analyze. The value is embedded inside a
// Markdown inline-code span: validateBranchName rejects backticks upstream,
// and `markdownSafeBranch` strips any remaining backtick here as defense in
// depth, so JSON.stringify's quote/escape handling is sufficient and the
// branch cannot break out of the span (#1996 tri-review P1).
defaultBranch: string = 'main',
): string {
const generatedRows =
generatedSkills && generatedSkills.length > 0
@@ -150,18 +174,18 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
## 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. For regression review, compare against the default branch: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\`.
- **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
@@ -430,6 +454,7 @@ export async function generateAIContextFiles(
options?.noStats,
options?.skipSkills,
runnerPath,
options?.defaultBranch ?? 'main',
);
const createdFiles: string[] = [];
@@ -472,3 +497,56 @@ export async function generateAIContextFiles(
return { files: createdFiles };
}
/**
* Refresh only the `base_ref: "..."` value inside the GitNexus block of an
* already-generated AGENTS.md / CLAUDE.md, in place (#1996 tri-review P2).
*
* The `alreadyUpToDate` analyze fast path returns before the normal
* {@link generateAIContextFiles} call, so a changed `.gitnexusrc` defaultBranch
* (or `--default-branch`) would otherwise not take effect until the next
* re-index. This does a surgical line update that preserves the rest of the
* block — including community-skill rows written by a prior `--skills` run —
* rather than regenerating (which would drop those rows on a no-`--skills` run).
*
* Best-effort: missing files, a missing/blank block, or a block with no
* `base_ref` line (e.g. a user-trimmed keep block) are silently skipped. Writes
* only when the value actually changes, so a routine up-to-date run is a no-op.
*/
export async function refreshBaseRefLine(
repoPath: string,
defaultBranch: string,
options?: { skipAgentsMd?: boolean },
): Promise<{ files: string[] }> {
if (options?.skipAgentsMd) return { files: [] };
const replacement = `base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}`;
const updated: string[] = [];
for (const name of ['AGENTS.md', 'CLAUDE.md']) {
const filePath = path.join(repoPath, name);
if (!(await fileExists(filePath))) continue;
let content: string;
try {
content = await fs.readFile(filePath, 'utf-8');
} catch {
continue;
}
const startIdx = findSectionMarkerIndex(content, GITNEXUS_START_MARKER);
if (startIdx === -1) continue;
const endIdx = findSectionMarkerIndex(content, GITNEXUS_END_MARKER, startIdx);
if (endIdx === -1 || endIdx <= startIdx) continue;
const blockEnd = endIdx + GITNEXUS_END_MARKER.length;
const block = content.substring(startIdx, blockEnd);
// Only the generated regression example carries a base_ref line, and only
// one per block; replace its quoted value while leaving the rest untouched.
const newBlock = block.replace(/base_ref: "(?:[^"\\]|\\.)*"/, replacement);
if (newBlock === block) continue; // no base_ref line present, or already current
const newContent = content.substring(0, startIdx) + newBlock + content.substring(blockEnd);
try {
await fs.writeFile(filePath, newContent, 'utf-8');
updated.push(name);
} catch {
// best-effort — never fail analyze over a context refresh
}
}
return { files: updated };
}
+426
View File
@@ -0,0 +1,426 @@
/**
* Project-local `.gitnexusrc` support for `gitnexus analyze` (#243).
*
* Lets a repository commit recurring `analyze` defaults — default branch for the
* generated regression example, AI-context / skills opt-outs, embedding knobs —
* so contributors don't re-pass the same flags on every run. Design rules:
*
* - Config is repo-local (`.gitnexusrc` at the resolved repo root). It is NOT
* read from `.gitnexus/` because that directory is index storage and is
* commonly gitignored.
* - JSON only. No YAML, no `package.json` field, no global `~/.gitnexus` file
* in this pass.
* - CLI flags always override config (see {@link mergeAnalyzeOptions}).
* - Fail closed: unknown keys, wrong value types, conflicting aliases, and
* invalid JSON all throw {@link GitNexusRcError} so a typo never silently
* no-ops. Errors are actionable and surface before any expensive analysis.
* - Config values never reach a shell. Strings are validated against control
* and hidden/bidirectional characters so they cannot inject markdown,
* JSON, or hidden controls into generated context files.
*
* Both a flat shape and a nested `analyze` block are accepted:
*
* { "defaultBranch": "develop", "skipContextFiles": true }
* { "analyze": { "defaultBranch": "develop", "skipSkills": true } }
*
* When both set the same option, the nested `analyze` block wins (deterministic,
* documented precedence). Conflicting *aliases at the same level* (e.g. both
* `defaultBranch` and `branch`) are rejected.
*/
import fs from 'node:fs';
import path from 'node:path';
import type { AnalyzeOptions } from './analyze.js';
export const GITNEXUS_RC_FILENAME = '.gitnexusrc';
/** Final fallback when no branch is configured or detectable. */
export const DEFAULT_BRANCH_FALLBACK = 'main';
/** Git refs longer than this are almost certainly a mistake / injection attempt. */
const BRANCH_MAX_LENGTH = 255;
/**
* Thrown for any `.gitnexusrc` problem (missing-file is NOT an error — it
* returns `undefined`). The message is user-facing and names the file so the
* CLI can print it verbatim before starting the progress bar.
*/
export class GitNexusRcError extends Error {
constructor(message: string) {
super(message);
this.name = 'GitNexusRcError';
}
}
type ValueKind =
| 'boolean'
| 'boolean-negate'
| 'string'
| 'numeric-string'
| 'embeddings'
| 'branch';
interface KeySpec {
/** The `AnalyzeOptions` field this config key normalizes into. */
target: keyof AnalyzeOptions;
kind: ValueKind;
}
/**
* Allowed `.gitnexusrc` keys and how each maps onto `AnalyzeOptions`.
*
* Aliases intentionally collapse onto a shared target:
* - `branch` is the legacy alias for `defaultBranch` (issue-comment shape).
* - `skipContextFiles` / `skipAiContext` are aliases for `skipAgentsMd` — they
* suppress the AGENTS.md / CLAUDE.md block ONLY. They do not imply
* `skipSkills`, and they are weaker than `indexOnly` (which skips all
* file injection). This matches the existing CLI semantics exactly.
* - `noStats` is the negation of `stats`.
*/
const KEY_SPECS: Record<string, KeySpec> = {
defaultBranch: { target: 'defaultBranch', kind: 'branch' },
branch: { target: 'defaultBranch', kind: 'branch' },
skipAgentsMd: { target: 'skipAgentsMd', kind: 'boolean' },
skipContextFiles: { target: 'skipAgentsMd', kind: 'boolean' },
skipAiContext: { target: 'skipAgentsMd', kind: 'boolean' },
skipSkills: { target: 'skipSkills', kind: 'boolean' },
indexOnly: { target: 'indexOnly', kind: 'boolean' },
stats: { target: 'stats', kind: 'boolean' },
noStats: { target: 'stats', kind: 'boolean-negate' },
embeddings: { target: 'embeddings', kind: 'embeddings' },
dropEmbeddings: { target: 'dropEmbeddings', kind: 'boolean' },
name: { target: 'name', kind: 'string' },
allowDuplicateName: { target: 'allowDuplicateName', kind: 'boolean' },
maxFileSize: { target: 'maxFileSize', kind: 'numeric-string' },
workerTimeout: { target: 'workerTimeout', kind: 'numeric-string' },
walCheckpointThreshold: { target: 'walCheckpointThreshold', kind: 'numeric-string' },
workers: { target: 'workers', kind: 'numeric-string' },
embeddingThreads: { target: 'embeddingThreads', kind: 'numeric-string' },
embeddingBatchSize: { target: 'embeddingBatchSize', kind: 'numeric-string' },
embeddingSubBatchSize: { target: 'embeddingSubBatchSize', kind: 'numeric-string' },
embeddingDevice: { target: 'embeddingDevice', kind: 'string' },
};
/** Top-level container key for the nested form; not itself an `AnalyzeOptions` field. */
const NESTED_KEY = 'analyze';
const ALLOWED_KEYS_HINT = `Allowed keys: ${Object.keys(KEY_SPECS).join(', ')} (or a nested "${NESTED_KEY}" object).`;
/**
* Reject control characters and hidden / bidirectional Unicode in a string
* value. These have no legitimate place in a branch name, registry name, or
* device string, and would otherwise let a committed config smuggle invisible
* controls into generated AGENTS.md / CLAUDE.md content.
*/
const isHiddenOrControl = (codePoint: number): boolean =>
codePoint < 0x20 ||
codePoint === 0x7f ||
(codePoint >= 0x200b && codePoint <= 0x200f) || // zero-width + LRM/RLM
(codePoint >= 0x202a && codePoint <= 0x202e) || // bidi embeddings/overrides
(codePoint >= 0x2060 && codePoint <= 0x2064) || // word-joiner + invisible math
(codePoint >= 0x2066 && codePoint <= 0x206f) || // bidi isolates + deprecated
codePoint === 0xfeff; // BOM / zero-width no-break space
const assertNoHiddenChars = (value: string, source: string): void => {
for (const ch of value) {
const cp = ch.codePointAt(0);
if (cp !== undefined && isHiddenOrControl(cp)) {
throw new GitNexusRcError(
`${source}: value contains control or hidden/bidirectional characters, which are not allowed.`,
);
}
}
};
/**
* Validate a user-supplied branch name (from CLI or `.gitnexusrc`). Returns the
* trimmed name or throws {@link GitNexusRcError}. Conservative but accepts the
* shapes real branches use (`feature/foo-bar`, `release/1.2`, `develop`).
*/
export function validateBranchName(value: string, source: string): string {
const trimmed = value.trim();
if (!trimmed) {
throw new GitNexusRcError(`${source}: branch name must not be empty.`);
}
if (trimmed.length > BRANCH_MAX_LENGTH) {
throw new GitNexusRcError(`${source}: branch name is too long (max ${BRANCH_MAX_LENGTH}).`);
}
assertNoHiddenChars(trimmed, source);
if (/\s/.test(trimmed)) {
throw new GitNexusRcError(`${source}: branch name must not contain whitespace.`);
}
// git ref-name rules (subset): reject characters git itself forbids in refs.
if (/[~^:?*[\\]/.test(trimmed)) {
throw new GitNexusRcError(
`${source}: branch name contains characters not allowed in a git ref (~ ^ : ? * [ \\).`,
);
}
if (trimmed.startsWith('-')) {
throw new GitNexusRcError(`${source}: branch name must not start with "-".`);
}
if (trimmed.includes('..')) {
throw new GitNexusRcError(`${source}: branch name must not contain "..".`);
}
// Git permits a backtick in a ref, but the branch is embedded inside a
// Markdown inline-code span in the generated AGENTS.md/CLAUDE.md regression
// example, where a backtick would close the span early and let the rest of
// the template render as instruction text. Reject it at this single
// chokepoint so all three tiers (CLI flag, .gitnexusrc, auto-detect via
// sanitizeDetectedBranch) are covered (#1996 tri-review P1).
if (trimmed.includes('`')) {
throw new GitNexusRcError(
`${source}: branch name must not contain a backtick (it would break the generated Markdown).`,
);
}
return trimmed;
}
/**
* Best-effort validation for an auto-detected branch (from git). Never throws —
* returns `undefined` for anything unusable so the resolver falls back to the
* next precedence tier.
*/
export function sanitizeDetectedBranch(value: string | null | undefined): string | undefined {
if (!value) return undefined;
try {
return validateBranchName(value, 'detected branch');
} catch {
return undefined;
}
}
const normalizeValue = (kind: ValueKind, value: unknown, key: string): unknown => {
const source = `${GITNEXUS_RC_FILENAME} "${key}"`;
switch (kind) {
case 'boolean':
if (typeof value !== 'boolean') {
throw new GitNexusRcError(`${source} must be a boolean (true/false).`);
}
return value;
case 'boolean-negate':
if (typeof value !== 'boolean') {
throw new GitNexusRcError(`${source} must be a boolean (true/false).`);
}
return !value;
case 'branch':
if (typeof value !== 'string') {
throw new GitNexusRcError(`${source} must be a string branch name.`);
}
return validateBranchName(value, source);
case 'string': {
if (typeof value !== 'string') {
throw new GitNexusRcError(`${source} must be a string.`);
}
const trimmed = value.trim();
if (!trimmed) {
throw new GitNexusRcError(`${source} must not be empty.`);
}
assertNoHiddenChars(trimmed, source);
// `name` flows into the generated AGENTS.md/CLAUDE.md as `**${name}**` and
// inside `gitnexus://repo/${name}/…` code spans, so a Markdown-significant
// character would break those spans or inject emphasis/links/HTML into
// agent-instruction content (#1996 tri-review P1). `_` is intentionally
// allowed (legitimate in repo names; intraword `_` is not emphasis).
// embeddingDevice (the other `string`-kind option) only ever holds a
// fixed device token, so this guard never rejects a valid value there.
if (/[`*[\]<>]/.test(trimmed)) {
throw new GitNexusRcError(
`${source} must not contain Markdown-significant characters (\` * [ ] < >).`,
);
}
return trimmed;
}
case 'numeric-string': {
// Mirror Commander's contract: these options reach the existing CLI
// validation as strings. Accept a JSON number or a string; normalize to a
// string and let the downstream per-flag validation enforce ranges so the
// error messages stay in one place.
if (typeof value === 'number') {
if (!Number.isFinite(value)) {
throw new GitNexusRcError(`${source} must be a finite number.`);
}
return String(value);
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) {
throw new GitNexusRcError(`${source} must not be empty.`);
}
return trimmed;
}
throw new GitNexusRcError(`${source} must be a number or numeric string.`);
}
case 'embeddings': {
// Mirror `--embeddings [limit]`: boolean toggles, a non-negative integer
// sets the node cap (normalized to a string, as Commander would supply).
if (typeof value === 'boolean') return value;
if (typeof value === 'number') {
if (!Number.isInteger(value) || value < 0) {
throw new GitNexusRcError(
`${source} must be true/false or a non-negative integer (node cap; 0 disables the cap).`,
);
}
return String(value);
}
throw new GitNexusRcError(
`${source} must be a boolean or a non-negative integer (node cap; 0 disables the cap).`,
);
}
default:
// Exhaustive — kept for forward-compat if a new kind is added.
throw new GitNexusRcError(`${source}: unsupported config value kind.`);
}
};
/**
* Normalize one level (flat top-level or the nested `analyze` block) into a
* partial `AnalyzeOptions`. Rejects unknown keys and two aliases that configure
* the same option at the same level.
*/
const normalizeLevel = (
obj: Record<string, unknown>,
{ allowNestedKey }: { allowNestedKey: boolean },
): Partial<AnalyzeOptions> => {
const out: Partial<AnalyzeOptions> = {};
const setBy = new Map<keyof AnalyzeOptions, string>();
for (const [key, value] of Object.entries(obj)) {
if (allowNestedKey && key === NESTED_KEY) continue; // handled separately
// `Object.hasOwn`, not a truthiness check: a plain-object lookup like
// `KEY_SPECS["__proto__"]` returns an inherited member (Object.prototype,
// truthy) and would slip past `if (!spec)`, hitting the wrong error branch
// instead of the documented "Unknown key" message (#1996 tri-review P3).
if (!Object.hasOwn(KEY_SPECS, key)) {
throw new GitNexusRcError(
`Unknown key "${key}" in ${GITNEXUS_RC_FILENAME}. ${ALLOWED_KEYS_HINT}`,
);
}
const spec = KEY_SPECS[key];
const prev = setBy.get(spec.target);
if (prev && prev !== key) {
throw new GitNexusRcError(
`${GITNEXUS_RC_FILENAME}: "${prev}" and "${key}" both configure the same option; set only one.`,
);
}
setBy.set(spec.target, key);
(out as Record<string, unknown>)[spec.target] = normalizeValue(spec.kind, value, key);
}
return out;
};
/**
* Locate, read, parse, validate, and normalize `.gitnexusrc` at `repoRoot`.
*
* @returns the normalized config defaults, or `undefined` when no file exists
* (the normal case). Throws {@link GitNexusRcError} on any problem.
*/
export function loadAnalyzeConfig(repoRoot: string): Partial<AnalyzeOptions> | undefined {
const filePath = path.join(repoRoot, GITNEXUS_RC_FILENAME);
let raw: string;
try {
raw = fs.readFileSync(filePath, 'utf-8');
} catch (err) {
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined;
throw new GitNexusRcError(`Could not read ${GITNEXUS_RC_FILENAME}: ${(err as Error).message}`);
}
// Strip a leading UTF-8 BOM: Node's 'utf-8' decode keeps it, and JSON.parse
// then fails with a confusing "Unexpected token" on an otherwise-valid file
// (#1996 tri-review). Only one leading BOM is stripped; in-string control
// rejection still applies to the parsed values.
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new GitNexusRcError(
`${GITNEXUS_RC_FILENAME} is not valid JSON: ${(err as Error).message}. ` +
`Expected a JSON object such as {"defaultBranch": "develop", "skipContextFiles": true}.`,
);
}
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new GitNexusRcError(`${GITNEXUS_RC_FILENAME} must contain a JSON object.`);
}
const obj = parsed as Record<string, unknown>;
const flat = normalizeLevel(obj, { allowNestedKey: true });
let nested: Partial<AnalyzeOptions> = {};
if (Object.prototype.hasOwnProperty.call(obj, NESTED_KEY)) {
const nestedRaw = obj[NESTED_KEY];
if (nestedRaw === null || typeof nestedRaw !== 'object' || Array.isArray(nestedRaw)) {
throw new GitNexusRcError(`${GITNEXUS_RC_FILENAME} "${NESTED_KEY}" must be a JSON object.`);
}
nested = normalizeLevel(nestedRaw as Record<string, unknown>, { allowNestedKey: false });
}
// Nested `analyze` block overrides flat top-level keys for the same option.
return { ...flat, ...nested };
}
/**
* Merge CLI options over `.gitnexusrc` defaults. CLI wins whenever it provides a
* value — including an explicit `false` (so an explicit CLI off-switch beats a
* config `true`). Config fills only the genuinely-unset (`undefined`) options.
*
* `stats` is special: Commander always materializes it (`true` by default,
* `false` only when `--no-stats` is passed), so plain `??` can't tell "default
* on" from "explicitly on". The rule is: a passed `--no-stats` (`stats: false`)
* always wins; otherwise the config value applies. There is intentionally no
* `--stats` counter-flag, so config can only turn stats off, not force it back
* on against a `--no-stats`.
*
* `defaultBranch` is NOT resolved here — see {@link resolveDefaultBranch}, which
* applies the CLI > config > auto-detect > "main" precedence chain.
*/
export function mergeAnalyzeOptions(
cli: AnalyzeOptions,
config: Partial<AnalyzeOptions> | undefined,
): AnalyzeOptions {
if (!config) return cli;
const merged: AnalyzeOptions = { ...cli };
for (const key of Object.keys(config) as (keyof AnalyzeOptions)[]) {
if (key === 'stats' || key === 'defaultBranch') continue; // handled below / by resolver
if (merged[key] === undefined) {
(merged as Record<string, unknown>)[key] = config[key];
}
}
if (config.stats !== undefined && cli.stats !== false) {
merged.stats = config.stats;
}
return merged;
}
/**
* Resolve the default branch threaded into generated context, applying the
* precedence chain:
*
* CLI `--default-branch` > `.gitnexusrc` `defaultBranch`/`branch`
* > auto-detected `origin/HEAD` > {@link DEFAULT_BRANCH_FALLBACK} ("main").
*
* User-supplied values (CLI, config) are validated strictly and throw on bad
* input. The auto-detected value is best-effort and silently ignored if
* unusable.
*/
export function resolveDefaultBranch(input: {
cliBranch?: string;
configBranch?: string;
detectedBranch?: string | null;
}): string {
if (input.cliBranch !== undefined) {
return validateBranchName(input.cliBranch, '--default-branch');
}
if (input.configBranch !== undefined) {
return validateBranchName(input.configBranch, `${GITNEXUS_RC_FILENAME} "defaultBranch"`);
}
const detected = sanitizeDetectedBranch(input.detectedBranch);
if (detected) return detected;
return DEFAULT_BRANCH_FALLBACK;
}
+253 -79
View File
@@ -9,6 +9,7 @@
*/
import path from 'path';
import os from 'os';
import { spawn } from 'child_process';
import v8 from 'v8';
import cliProgress from 'cli-progress';
@@ -26,7 +27,14 @@ import {
AnalysisNotFinalizedError,
assertAnalysisFinalized,
} from '../storage/repo-manager.js';
import { getGitRoot, hasGitDir } from '../storage/git.js';
import { getGitRoot, hasGitDir, getDefaultBranch } from '../storage/git.js';
import {
loadAnalyzeConfig,
mergeAnalyzeOptions,
resolveDefaultBranch,
validateBranchName,
GitNexusRcError,
} from './analyze-config.js';
import { runFullAnalysis } from '../core/run-analyze.js';
import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
import { warnMissingOptionalGrammars } from './optional-grammars.js';
@@ -35,6 +43,7 @@ import fs from 'fs/promises';
import { cliError } from './cli-message.js';
import { formatElapsed } from './format-elapsed.js';
import { isHfDownloadFailure } from '../core/embeddings/hf-env.js';
import { isLocalEmbeddingRuntimeBlockerMessage } from '../core/embeddings/runtime-support.js';
import { warnIfNpm11NpxRisk } from './resolve-invocation.js';
// Capture stderr.write at module load BEFORE anything (LadybugDB native
@@ -51,6 +60,22 @@ const writeFatalToStderr = (label: string, err: unknown): void => {
const message = isErr ? err.message : String(err);
realStderrWrite(`\n ${label}: ${message}\n`);
if (isErr && err.stack) realStderrWrite(`${err.stack}\n`);
// Walk and print the `cause` chain. The phase runner wraps the underlying
// failure as `new Error("Phase 'X' failed: …", { cause })`, so the original
// error (e.g. a WorkerPoolDispatchError carrying the worker-side stack from
// #2068) is only reachable via `.cause`. Without this the user sees the
// wrapper's main-thread stack and never the real frame. `cause.stack` already
// begins with the cause's message, so we print the stack alone (not message +
// stack) to avoid repeating it. Depth-bounded so a cyclic `cause` can't loop
// (the phase runner wraps one level; the bound leaves headroom for future
// nesting); uses realStderrWrite so the redirected console.error's ANSI
// clear-line wrapping can't erase it (#1169).
const MAX_CAUSE_DEPTH = 5;
let cause: unknown = isErr ? (err as { cause?: unknown }).cause : undefined;
for (let depth = 0; depth < MAX_CAUSE_DEPTH && cause instanceof Error; depth++) {
realStderrWrite(`\n Caused by: ${cause.stack ?? cause.message}\n`);
cause = (cause as { cause?: unknown }).cause;
}
};
let fatalHandlersInstalled = false;
@@ -76,13 +101,45 @@ const installFatalHandlers = (): void => {
});
};
const HEAP_MB = 16384;
/** Historical floor for the re-exec heap cap — the auto-sizer never goes below
* this, so small boxes / CI never regress. */
const DEFAULT_HEAP_MB = 16384;
/**
* RAM-aware re-exec heap cap (MB): `0.75 × effective RAM`, clamped to
* `>= DEFAULT_HEAP_MB`. Kept BELOW physical RAM on purpose — a cap `>=` RAM makes
* V8 collect lazily and inflate the heap into swap-thrash (observed analyzing the
* Linux kernel at a 30GB cap on a 31GB box). `constrainedBytes` is the cgroup
* limit or `null`; it is honored only as a real, smaller-than-physical cap, because
* `process.constrainedMemory()` returns a huge sentinel when UNCONSTRAINED.
*/
export function computeHeapCapMb(totalBytes: number, constrainedBytes: number | null): number {
const effectiveBytes =
constrainedBytes !== null && constrainedBytes > 0 && constrainedBytes < totalBytes
? constrainedBytes
: totalBytes;
const effectiveMb = Math.floor(effectiveBytes / (1024 * 1024));
return Math.max(DEFAULT_HEAP_MB, Math.floor(0.75 * effectiveMb));
}
function readConstrainedBytes(): number | null {
if (typeof process.constrainedMemory !== 'function') return null;
const c = process.constrainedMemory();
return typeof c === 'number' && c > 0 ? c : null;
}
const HEAP_MB = computeHeapCapMb(os.totalmem(), readConstrainedBytes());
const TEST_RESPAWN_HEAP_MB = Number(process.env.GITNEXUS_TEST_RESPAWN_HEAP_MB);
const RESPAWN_HEAP_MB =
Number.isFinite(TEST_RESPAWN_HEAP_MB) && TEST_RESPAWN_HEAP_MB > 0
? Math.floor(TEST_RESPAWN_HEAP_MB)
: HEAP_MB;
const HEAP_FLAG = `--max-old-space-size=${RESPAWN_HEAP_MB}`;
/** Larger semi-space (young-gen) cuts minor-GC frequency + promotion churn during
* the multi-million-node graph build/emit. Allowed in NODE_OPTIONS (unlike
* --stack-size), so it propagates to the re-exec env cleanly. */
const SEMI_SPACE_MB = 128;
const SEMI_FLAG = `--max-semi-space-size=${SEMI_SPACE_MB}`;
/** Increase default stack size (KB) to prevent stack overflow on deep class hierarchies. */
const STACK_KB = 4096;
const STACK_FLAG = `--stack-size=${STACK_KB}`;
@@ -432,7 +489,8 @@ const forceHeapOOMForTestIfEnabled = (): void => {
// `gitnexus/src/core/lbug/lbug-config.ts` in sync with this value.
const RECOMMENDED_WAL_CHECKPOINT_THRESHOLD = 64 * 1024 * 1024;
/** Re-exec the process with a 16GB heap and larger stack if we're currently below that. */
/** Re-exec the process with the RAM-aware auto heap cap + larger semi-space/stack
* if we're currently below that. A user-supplied NODE_OPTIONS heap wins (no re-exec). */
async function ensureHeap(): Promise<boolean> {
const nodeOpts = process.env.NODE_OPTIONS || '';
if (nodeOpts.includes('--max-old-space-size')) return false;
@@ -440,25 +498,26 @@ async function ensureHeap(): Promise<boolean> {
const v8Heap = v8.getHeapStatistics().heap_size_limit;
if (v8Heap >= HEAP_MB * 1024 * 1024 * 0.9) return false;
// --stack-size is a V8 flag not allowed in NODE_OPTIONS on Node 24+,
// so pass it only as a direct CLI argument, not via the environment.
const cliFlags = [HEAP_FLAG];
// --stack-size is a V8 flag not allowed in NODE_OPTIONS on Node 24+, so pass it
// only as a direct CLI argument. --max-semi-space-size IS allowed in NODE_OPTIONS.
const cliFlags = [HEAP_FLAG, SEMI_FLAG];
if (!nodeOpts.includes('--stack-size')) cliFlags.push(STACK_FLAG);
const childArgs = [...cliFlags, ...process.argv.slice(1)];
const childEnv = {
...process.env,
NODE_OPTIONS: `${nodeOpts} ${HEAP_FLAG}`.trim(),
NODE_OPTIONS: `${nodeOpts} ${HEAP_FLAG} ${SEMI_FLAG}`.trim(),
};
if (shouldBridgeRespawnProgressTty()) childEnv[RESPAWN_PROGRESS_ENV] = '1';
const childExit = await runRespawnedAnalyze(childArgs, childEnv);
if (childExit.status !== 0 || childExit.signal) {
if (childProcessLikelyOom(childExit)) {
cliError(
` Analysis likely ran out of memory.\n` +
` Retry with a larger heap if your machine allows it:\n` +
` NODE_OPTIONS="--max-old-space-size=24576" gitnexus analyze [your-args]\n` +
` (Windows: set NODE_OPTIONS=--max-old-space-size=24576 && gitnexus analyze [your-args])\n` +
` Analysis likely ran out of memory (heap cap auto-sized to ${RESPAWN_HEAP_MB}MB ≈ 0.75x RAM).\n` +
` This repository's working set exceeds available RAM. Use a machine with more RAM,\n` +
` or override the cap (a cap above physical RAM causes swap-thrash — use with care):\n` +
` NODE_OPTIONS="--max-old-space-size=<MB>" gitnexus analyze [your-args]\n` +
` (Windows: set NODE_OPTIONS=--max-old-space-size=<MB> && gitnexus analyze [your-args])\n` +
` If this persists, it may be a native crash unrelated to heap size.\n`,
{ recoveryHint: 'heap-oom-respawn' },
);
@@ -466,8 +525,7 @@ async function ensureHeap(): Promise<boolean> {
cliError(
` Analysis aborted in a native worker or native binding path.\n` +
` Try one of these recovery paths:\n` +
` gitnexus analyze --workers 0\n` +
` npm uninstall -g gitnexus && npm install -g gitnexus@latest\n` +
` npm uninstall -g gitnexus && npm install -g gitnexus@latest (rebuilds native bindings)\n` +
` Use Node 22 LTS if you are on a newer non-LTS runtime.\n`,
{ recoveryHint: 'native-worker-abort' },
);
@@ -492,6 +550,7 @@ const ANALYZE_CLI_ENV_KEYS = [
'GITNEXUS_VERBOSE',
'GITNEXUS_PROFILE_DEFERRED',
'GITNEXUS_PROFILE_DEFERRED_SLOW_MS',
'GITNEXUS_DEBUG_HEAP',
'GITNEXUS_MAX_FILE_SIZE',
'GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS',
'GITNEXUS_WAL_CHECKPOINT_THRESHOLD',
@@ -554,6 +613,13 @@ export interface AnalyzeOptions {
stats?: boolean;
/** Skip installing standard GitNexus skill files to .claude/skills/gitnexus/. */
skipSkills?: boolean;
/**
* Default branch for the generated regression-compare example (#243). From
* `--default-branch`; may also be supplied via `.gitnexusrc`. Resolved to a
* concrete branch (CLI > `.gitnexusrc` > auto-detected origin/HEAD > "main")
* before being threaded into the generated AGENTS.md / CLAUDE.md content.
*/
defaultBranch?: string;
/** Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills). */
indexOnly?: boolean;
/** Index the folder even when no .git directory is present. */
@@ -583,7 +649,7 @@ export interface AnalyzeOptions {
workerTimeout?: string;
/** Control LadybugDB WAL auto-checkpoint threshold during analyze. */
walCheckpointThreshold?: string;
/** Parse worker pool size; 0 disables workers (sequential fallback). */
/** Parse worker pool size (>=1); 0 is rejected (no sequential mode). */
workers?: string;
embeddingThreads?: string;
embeddingBatchSize?: string;
@@ -638,16 +704,116 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption
}
};
const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions): Promise<void> => {
if (options?.verbose) {
const analyzeCommandImpl = async (
inputPath?: string,
cliOptions?: AnalyzeOptions,
): Promise<void> => {
console.log('\n GitNexus Analyzer\n');
// ── Resolve the target repo root ──────────────────────────────────
// Resolved FIRST because `.gitnexusrc` is read from the repo root (not the
// caller's cwd), and config can set defaults that the validation below
// consumes. `--skip-git` is a CLI-only flag (never a config key), so the raw
// CLI options are authoritative for repo-root resolution.
let repoPath: string;
if (inputPath) {
repoPath = path.resolve(inputPath);
} else if (cliOptions?.skipGit) {
// --skip-git: treat cwd as the index root, do not walk up to a parent git repo.
repoPath = path.resolve(process.cwd());
} else {
const gitRoot = getGitRoot(process.cwd());
if (!gitRoot) {
console.log(
' Not inside a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n',
);
process.exitCode = 1;
return;
}
repoPath = gitRoot;
}
const repoHasGit = hasGitDir(repoPath);
if (!repoHasGit && !cliOptions?.skipGit) {
console.log(
' Not a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n',
);
process.exitCode = 1;
return;
}
if (!repoHasGit) {
console.log(
' Warning: no .git directory found — commit-tracking and incremental updates disabled.\n',
);
}
// Validate an explicit `--default-branch` up front so its errors are
// attributed to the flag (with a CLI-specific recovery hint) rather than to
// `.gitnexusrc`, which the user may not even have (#1996 tri-review).
if (cliOptions?.defaultBranch !== undefined) {
try {
validateBranchName(cliOptions.defaultBranch, '--default-branch');
} catch (err) {
cliError(` ${err instanceof Error ? err.message : String(err)}\n`, {
recoveryHint: 'default-branch-invalid',
});
process.exitCode = 1;
return;
}
}
// ── Load .gitnexusrc and merge: CLI flags override config (#243) ───
// Parse/validate before the progress bar so a malformed config produces an
// actionable error and exits before any expensive analysis starts.
let options: AnalyzeOptions;
let resolvedDefaultBranch: string;
try {
const fileConfig = loadAnalyzeConfig(repoPath);
options = mergeAnalyzeOptions(cliOptions ?? {}, fileConfig);
// Resolve the default branch threaded into generated context:
// CLI --default-branch > .gitnexusrc defaultBranch/branch
// > auto-detected origin/HEAD > "main".
// Only shell out to git when no branch was configured AND the generated
// context will actually use it, keeping the common path free of an extra
// git call. Detection is best-effort and never blocks analyze.
const cliBranch = cliOptions?.defaultBranch;
const configBranch = fileConfig?.defaultBranch;
const willGenerateContext = !options.indexOnly && !options.skipAgentsMd;
let detectedBranch: string | null = null;
if (
cliBranch === undefined &&
configBranch === undefined &&
repoHasGit &&
!cliOptions?.skipGit &&
willGenerateContext
) {
try {
detectedBranch = getDefaultBranch(repoPath);
} catch {
detectedBranch = null;
}
}
resolvedDefaultBranch = resolveDefaultBranch({ cliBranch, configBranch, detectedBranch });
} catch (err) {
const msg =
err instanceof GitNexusRcError
? err.message
: `Invalid .gitnexusrc: ${err instanceof Error ? err.message : String(err)}`;
cliError(` ${msg}\n`, { recoveryHint: 'gitnexusrc-invalid' });
process.exitCode = 1;
return;
}
if (options.verbose) {
process.env.GITNEXUS_VERBOSE = '1';
}
if (options?.maxFileSize) {
if (options.maxFileSize) {
process.env.GITNEXUS_MAX_FILE_SIZE = options.maxFileSize;
}
if (options?.workerTimeout) {
if (options.workerTimeout) {
const workerTimeoutSeconds = Number(options.workerTimeout);
if (!Number.isFinite(workerTimeoutSeconds) || workerTimeoutSeconds < 1) {
cliError(' --worker-timeout must be at least 1 second.\n');
@@ -659,7 +825,7 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
);
}
if (options?.walCheckpointThreshold !== undefined) {
if (options.walCheckpointThreshold !== undefined) {
const parsed = parseWalCheckpointThreshold(options.walCheckpointThreshold);
if (parsed === undefined) {
cliError(' --wal-checkpoint-threshold must be an integer >= -1.\n');
@@ -676,12 +842,13 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
// values back-to-back and observe the value they passed, not whatever the
// previous call leaked.
let workerPoolSize: number | undefined;
if (options?.workers !== undefined) {
if (options.workers !== undefined) {
const parsedWorkers = Number(options.workers);
if (!Number.isInteger(parsedWorkers) || parsedWorkers < 0) {
if (!Number.isInteger(parsedWorkers) || parsedWorkers < 1) {
cliError(
' --workers must be a non-negative integer. ' +
'Pass 0 to disable the worker pool (sequential fallback).\n',
' --workers must be a positive integer (>= 1). ' +
'GitNexus parses through a worker pool only — there is no sequential ' +
'mode, so 0 is not allowed. Omit --workers for an auto-sized pool.\n',
);
process.exitCode = 1;
return;
@@ -694,7 +861,7 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
// sibling-validation pattern (exit before bar.start() — otherwise
// process.exit() leaves the progress bar's hidden cursor uncleared).
let embeddingsNodeLimit: number | undefined;
if (typeof options?.embeddings === 'string') {
if (typeof options.embeddings === 'string') {
const parsed = Number(options.embeddings);
if (!Number.isInteger(parsed) || parsed < 0) {
cliError(
@@ -706,7 +873,7 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
}
embeddingsNodeLimit = parsed;
}
const embeddingsEnabled = !!options?.embeddings;
const embeddingsEnabled = !!options.embeddings;
const setPositiveEnv = (
optionName: string,
@@ -728,23 +895,23 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
!setPositiveEnv(
'--embedding-threads',
'GITNEXUS_EMBEDDING_THREADS',
options?.embeddingThreads,
options.embeddingThreads,
) ||
!setPositiveEnv(
'--embedding-batch-size',
'GITNEXUS_EMBEDDING_BATCH_SIZE',
options?.embeddingBatchSize,
options.embeddingBatchSize,
) ||
!setPositiveEnv(
'--embedding-sub-batch-size',
'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE',
options?.embeddingSubBatchSize,
options.embeddingSubBatchSize,
)
) {
return;
}
if (options?.embeddingDevice) {
if (options.embeddingDevice) {
const allowed = new Set(['auto', 'cpu', 'dml', 'cuda', 'wasm']);
if (!allowed.has(options.embeddingDevice)) {
cliError(' --embedding-device must be one of: auto, cpu, dml, cuda, wasm.\n');
@@ -754,7 +921,7 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
process.env.GITNEXUS_EMBEDDING_DEVICE = options.embeddingDevice;
}
if (options?.repairFts && options?.force) {
if (options.repairFts && options.force) {
cliError(
' Cannot combine `--repair-fts` with `--force`. ' +
'Use `--repair-fts` for fast FTS-only repair, or `--force` for a full rebuild.\n',
@@ -763,52 +930,18 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
return;
}
console.log('\n GitNexus Analyzer\n');
// `--index-only` is the stronger contract — it suppresses every form of file
// injection, including community skill writes that `--skills` would normally
// produce. Surface the override explicitly so users don't wonder why a
// pipeline re-index ran but no skill files appeared. The pipeline still
// re-runs (see `force: options?.force || options?.skills` below); the warning
// re-runs (see `force: options.force || options.skills` below); the warning
// is purely about the dropped post-index write step.
if (options?.indexOnly && options?.skills) {
if (options.indexOnly && options.skills) {
console.log(
' Note: --index-only overrides --skills; community skill files will not be written.\n',
);
}
let repoPath: string;
if (inputPath) {
repoPath = path.resolve(inputPath);
} else if (options?.skipGit) {
// --skip-git: treat cwd as the index root, do not walk up to a parent git repo.
repoPath = path.resolve(process.cwd());
} else {
const gitRoot = getGitRoot(process.cwd());
if (!gitRoot) {
console.log(
' Not inside a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n',
);
process.exitCode = 1;
return;
}
repoPath = gitRoot;
}
const repoHasGit = hasGitDir(repoPath);
if (!repoHasGit && !options?.skipGit) {
console.log(
' Not a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n',
);
process.exitCode = 1;
return;
}
if (!repoHasGit) {
console.log(
' Warning: no .git directory found \u2014 commit-tracking and incremental updates disabled.\n',
);
}
// If the target repo contains files an optional grammar would parse but
// that grammar's native binding is absent, warn before analysis so users
// learn why those files end up unparsed instead of silently getting a
@@ -938,36 +1071,39 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
// ── Run shared analysis orchestrator ───────────────────────────────
try {
const skipAll = options?.indexOnly;
const skipAgentsMd = skipAll || options?.skipAgentsMd;
const skipSkills = skipAll || options?.skipSkills;
const skipAll = options.indexOnly;
const skipAgentsMd = skipAll || options.skipAgentsMd;
const skipSkills = skipAll || options.skipSkills;
const result = await runFullAnalysis(
repoPath,
{
// Pipeline re-index — OR'd with --skills because skill generation
// needs a fresh pipelineResult. Has no bearing on the registry
// collision guard (see allowDuplicateName below).
force: options?.force || options?.skills,
repairFts: options?.repairFts,
force: options.force || options.skills,
repairFts: options.repairFts,
embeddings: embeddingsEnabled,
embeddingsNodeLimit,
dropEmbeddings: options?.dropEmbeddings,
verbose: options?.verbose,
skipGit: options?.skipGit,
dropEmbeddings: options.dropEmbeddings,
verbose: options.verbose,
skipGit: options.skipGit,
skipAgentsMd,
skipSkills,
// Resolved default branch (CLI > .gitnexusrc > auto-detect > "main")
// threaded into the generated regression-compare example (#243).
defaultBranch: resolvedDefaultBranch,
// commander.js `.option('--no-stats', …)` registers the flag as
// `options.stats` (boolean, default true; `false` when the user
// passed --no-stats). Reading `options?.noStats` here returns
// passed --no-stats). Reading `options.noStats` here returns
// undefined every time, so the flag was a no-op on the markdown
// rewrite path before this fix. See #1477.
noStats: options?.stats === false,
registryName: options?.name,
noStats: options.stats === false,
registryName: options.name,
// Registry-collision bypass — its own CLI flag, intentionally NOT
// overloading --force. A user who hits the collision guard should
// be able to accept the duplicate name without also paying the
// cost of a full pipeline re-index. See #829 review round 2.
allowDuplicateName: options?.allowDuplicateName,
allowDuplicateName: options.allowDuplicateName,
// Worker pool size threaded from --workers, replacing the previous
// GITNEXUS_WORKER_POOL_SIZE env mutation. `undefined` defers to the
// env / auto-formula fallback inside the pipeline.
@@ -987,6 +1123,21 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
// that half-finalized state, runFullAnalysis returns alreadyUpToDate
// on the next invocation unless we check the registry here too.
await assertAnalysisFinalized(repoPath);
// The fast path skips context regeneration, but a changed `.gitnexusrc`
// defaultBranch / `--default-branch` must still take effect. Surgically
// refresh just the `base_ref` line in AGENTS.md/CLAUDE.md in place,
// preserving the rest of the block (incl. --skills community rows). No-op
// when the value already matches, so a routine up-to-date run is silent
// (#1996 tri-review P2).
let baseRefRefreshed: string[] = [];
try {
const { refreshBaseRefLine } = await import('./ai-context.js');
baseRefRefreshed = (
await refreshBaseRefLine(repoPath, resolvedDefaultBranch, { skipAgentsMd })
).files;
} catch {
/* best-effort — never fail the fast path over a context refresh */
}
clearInterval(elapsedTimer);
process.removeListener('SIGINT', sigintHandler);
console.log = origLog;
@@ -996,6 +1147,11 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
console.error = origError;
bar.stop();
console.log(' Already up to date\n');
if (baseRefRefreshed.length > 0) {
console.log(
` Updated base_ref to "${resolvedDefaultBranch}" in ${baseRefRefreshed.join(', ')}\n`,
);
}
// Safe to return without process.exit(0) — the early-return path in
// runFullAnalysis never opens LadybugDB, so no native handles prevent exit.
return;
@@ -1069,9 +1225,13 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
{
skipAgentsMd,
skipSkills,
// Same resolved branch as the main run (#243) so the --skills
// re-generation of AGENTS.md/CLAUDE.md does not revert base_ref
// to "main".
defaultBranch: resolvedDefaultBranch,
// Mirror runFullAnalysis `noStats` bridge (#1477) — same expression;
// exercised on the `--skills` path by analyze-no-stats-bridge.test.ts.
noStats: options?.stats === false,
noStats: options.stats === false,
},
);
}
@@ -1191,6 +1351,20 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
return;
}
// Local embedding runtime unsupported on this platform (macOS Intel ships no
// darwin/x64 ONNX native binding, #1515). The guard threw before importing
// transformers.js, so this is a clean, actionable GitNexus message. Checked
// before the network-heuristic isHfDownloadFailure branch below (and before
// the generic module-not-found "installation may be corrupt" hint) so the
// explicit platform message always takes priority.
if (isLocalEmbeddingRuntimeBlockerMessage(msg)) {
cliError(` ${msg.replace(/\n/g, '\n ')}\n`, {
recoveryHint: 'local-embedding-unsupported',
});
process.exitCode = 1;
return;
}
// HF download failure — show clean guidance without the raw stack trace.
// Checked before writeFatalToStderr so the user sees one focused message
// rather than a stack-trace dump followed by a second remediation block.
+4 -1
View File
@@ -49,9 +49,12 @@ export type RecoveryHint =
| 'heap-oom-respawn'
| 'native-worker-abort'
| 'hf-endpoint-unreachable'
| 'local-embedding-unsupported'
| 'large-repo'
| 'npm-resolution'
| 'module-not-found';
| 'module-not-found'
| 'gitnexusrc-invalid'
| 'default-branch-invalid';
/**
* Common shape for the optional structured-field bag passed to
+37
View File
@@ -1,6 +1,7 @@
import { getRuntimeCapabilities, getRuntimeFingerprint } from '../core/platform/capabilities.js';
import { resolveEmbeddingConfig } from '../core/embeddings/config.js';
import { isHttpMode } from '../core/embeddings/http-client.js';
import { getLocalEmbeddingRuntimeBlocker } from '../core/embeddings/runtime-support.js';
import { checkLbugNative } from '../core/lbug/native-check.js';
import { getExtensionInstallPolicy } from '../core/lbug/extension-loader.js';
import { t } from './i18n/index.js';
@@ -50,6 +51,33 @@ export function padDisplayEnd(value: string, columns: number): string {
const label = (key: Parameters<typeof t>[0], width: number): string => padDisplayEnd(t(key), width);
/**
* Embedding-runtime support status for the `doctor` Embeddings section.
* Pure and DI-friendly so it can be unit-tested without running the whole
* command. Delegates the platform decision to
* {@link getLocalEmbeddingRuntimeBlocker} so the wording stays in one place.
*
* - HTTP mode: always supported (never touches the native runtime).
* - Local mode on an unsupported platform (macOS Intel, #1515): reports the
* blocker as `detail` so the caller can surface the full guidance.
*/
export function localEmbeddingDoctorStatus(opts: {
httpMode: boolean;
platform?: NodeJS.Platform;
arch?: NodeJS.Architecture;
}): { status: string; detail: string | null } {
if (opts.httpMode) {
return { status: '✓ http endpoint configured', detail: null };
}
const platform = opts.platform ?? process.platform;
const arch = opts.arch ?? process.arch;
const blocker = getLocalEmbeddingRuntimeBlocker({ platform, arch });
if (blocker) {
return { status: `✗ local embeddings unavailable on ${platform}/${arch}`, detail: blocker };
}
return { status: '✓ local embeddings supported', detail: null };
}
export const doctorCommand = async () => {
const fingerprint = getRuntimeFingerprint();
const capabilities = getRuntimeCapabilities();
@@ -102,4 +130,13 @@ export const doctorCommand = async () => {
console.log(
` ${label('doctor.labels.subBatch', 12)}${t('doctor.chunks', { count: embeddingConfig.subBatchSize })}`,
);
// Surface local-runtime support so macOS Intel users see up front that local
// embeddings can't load here (the bundled ONNX Runtime ships no darwin/x64
// native binding, #1515) — rather than discovering it only when
// `analyze --embeddings` fails. Literal label like the 'native' line above.
const support = localEmbeddingDoctorStatus({ httpMode: isHttpMode() });
console.log(` ${padDisplayEnd('Support:', 12)}${support.status}`);
if (support.detail) {
process.stderr.write(`\n${support.detail.replace(/^/gm, ' ')}\n\n`);
}
};
+2 -2
View File
@@ -175,7 +175,7 @@ export const en = {
'help.option.analyze.walCheckpointThreshold':
'LadybugDB WAL auto-checkpoint threshold in bytes during analyze (integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).',
'help.option.analyze.workers':
'Parse worker pool size. Default: cores-1 capped at 16. Pass 0 to disable workers (sequential).',
'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.',
'help.option.analyze.embeddingThreads': 'Limit local ONNX embedding CPU threads',
'help.option.analyze.embeddingBatchSize': 'Number of nodes per embedding batch',
'help.option.analyze.embeddingSubBatchSize': 'Number of chunks per embedding model call',
@@ -189,7 +189,7 @@ export const en = {
'help.option.clean.lbugSidecars': 'Clean quarantined LadybugDB missing-shadow WAL sidecars',
'help.option.wiki.force': 'Force full regeneration even if up to date',
'help.option.wiki.provider':
'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
'help.option.wiki.baseUrl':
'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
+2 -2
View File
@@ -164,7 +164,7 @@ export const zhCN = {
'help.option.analyze.walCheckpointThreshold':
'analyze 期间 LadybugDB WAL 自动 checkpoint 阈值(字节,整数 >= -1;默认:67108864 = 64 MiB;-1 保持 Ladybug 默认约 16 MiB)。',
'help.option.analyze.workers':
'解析 worker 池大小。默认:cores-1,最多 16。传 0 禁用 worker(顺序执行)。',
'解析 worker 池大小(>=1)。默认:cores-1,最多 16,按仓库规模自适应。',
'help.option.analyze.embeddingThreads': '限制本地 ONNX 嵌入 CPU 线程数',
'help.option.analyze.embeddingBatchSize': '每个嵌入批次的节点数',
'help.option.analyze.embeddingSubBatchSize': '每次嵌入模型调用的分块数',
@@ -178,7 +178,7 @@ export const zhCN = {
'help.option.clean.lbugSidecars': '清理已隔离的 LadybugDB missing-shadow WAL sidecar',
'help.option.wiki.force': '即使已是最新也强制完整重新生成',
'help.option.wiki.provider':
'LLM 提供商:openai、openrouter、azure、custom、cursor、claude 或 codex(默认:openai)',
'LLM 提供商:openai、openrouter、azure、custom、cursor、claude、codex 或 opencode(默认:openai)',
'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
'help.option.wiki.baseUrl':
'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
+7 -2
View File
@@ -44,6 +44,11 @@ program
'(no-op when --index-only is also set).',
)
.option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
.option(
'--default-branch <branch>',
'Default branch used in the generated regression-compare example (base_ref). ' +
'Falls back to .gitnexusrc, then auto-detected origin/HEAD, then "main".',
)
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
.option(
'--skip-skills',
@@ -82,7 +87,7 @@ program
)
.option(
'--workers <n>',
'Parse worker pool size. Default: cores-1 capped at 16. Pass 0 to disable workers (sequential).',
'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.',
)
.option('--embedding-threads <n>', 'Limit local ONNX embedding CPU threads')
.option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
@@ -150,7 +155,7 @@ program
.option('-f, --force', 'Force full regeneration even if up to date')
.option(
'--provider <provider>',
'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
'LLM provider: openai, openrouter, azure, custom, cursor, claude, codex, or opencode (default: openai)',
)
.option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
.option(
+2 -2
View File
@@ -649,9 +649,9 @@ const renderSkillMarkdown = (
: community.label;
lines.push('## How to Explore');
lines.push('');
lines.push(`1. \`gitnexus_context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
lines.push(`1. \`context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
lines.push(
`2. \`gitnexus_query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`,
`2. \`query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`,
);
lines.push('3. Read key files listed above for implementation details');
lines.push('');
+23 -7
View File
@@ -58,14 +58,21 @@ function parsePositiveIntegerOption(
function isLocalProvider(
provider: LLMProvider | undefined,
): provider is 'cursor' | 'claude' | 'codex' {
return provider === 'cursor' || provider === 'claude' || provider === 'codex';
): provider is 'cursor' | 'claude' | 'codex' | 'opencode' {
return (
provider === 'cursor' ||
provider === 'claude' ||
provider === 'codex' ||
provider === 'opencode'
);
}
function localModelConfigKey(provider: 'cursor' | 'claude' | 'codex') {
function localModelConfigKey(provider: 'cursor' | 'claude' | 'codex' | 'opencode') {
if (provider === 'cursor') return 'cursorModel';
if (provider === 'claude') return 'claudeModel';
return 'codexModel';
if (provider === 'codex') return 'codexModel';
if (provider === 'opencode') return 'opencodeModel';
throw new Error(`Unsupported local provider: ${provider satisfies never}`);
}
/**
@@ -248,7 +255,7 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
console.log(' Error: No LLM API key found.');
console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex.\n');
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
process.exitCode = 1;
return;
}
@@ -256,16 +263,17 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
} else {
console.log(" No LLM configured. Let's set it up.\n");
console.log(
' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, or Codex CLI.\n',
' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, Codex CLI, or OpenCode CLI.\n',
);
// Check if local agent CLIs are available.
const hasCursor = detectCursorCLI();
const hasClaude = detectLocalCLI('claude');
const hasCodex = detectLocalCLI('codex');
const hasOpenCode = detectLocalCLI('opencode');
const localChoices: Array<{
choice: string;
provider: 'cursor' | 'claude' | 'codex';
provider: 'cursor' | 'claude' | 'codex' | 'opencode';
}> = [];
// Provider selection
@@ -298,6 +306,14 @@ const wikiCommandImpl = async (inputPath?: string, options?: WikiCommandOptions)
});
console.log(` [${choice}] Codex CLI (local, uses your Codex login)`);
}
if (hasOpenCode) {
const choice = String(nextChoice++);
localChoices.push({
choice,
provider: 'opencode',
});
console.log(` [${choice}] OpenCode CLI (local, uses your OpenCode login/config)`);
}
console.log('');
const maxChoice = String(nextChoice - 1);
+21 -6
View File
@@ -14,12 +14,11 @@ if (!process.env.ORT_LOG_LEVEL) {
process.env.ORT_LOG_LEVEL = '3';
}
import {
pipeline,
env,
type FeatureExtractionPipeline,
type ProgressInfo,
} from '@huggingface/transformers';
// Type-only import: erased at compile time so loading this module never pulls
// in @huggingface/transformers (and its native onnxruntime-node binding) at
// runtime. The runtime values (pipeline, env) are dynamically imported inside
// initEmbedder, after the platform guard has passed (#1515).
import type { FeatureExtractionPipeline, ProgressInfo } from '@huggingface/transformers';
import { existsSync } from 'fs';
import { execFileSync } from 'child_process';
import { join, dirname } from 'path';
@@ -28,6 +27,7 @@ import { DEFAULT_EMBEDDING_CONFIG, type EmbeddingConfig, type ModelProgress } fr
import { isHttpMode, getHttpDimensions, httpEmbed } from './http-client.js';
import { resolveEmbeddingConfig } from './config.js';
import { applyHfEnvOverrides, isHfDownloadFailure, withHfDownloadRetry } from './hf-env.js';
import { getLocalEmbeddingRuntimeBlocker } from './runtime-support.js';
import { logger } from '../logger.js';
/**
@@ -143,6 +143,17 @@ export const initEmbedder = async (
);
}
// Fail fast on platforms where the bundled native ONNX Runtime binding is not
// shipped (macOS Intel, #1515). Must run before any transformers.js /
// onnxruntime-node import or resolution — otherwise the native module load
// crashes with a raw "Cannot find module ...onnxruntime_binding.node" that
// ONNX_WEB_BACKEND=wasm cannot rescue (#1516). HTTP mode was already handled
// above, so this only blocks the local-runtime path.
const runtimeBlocker = getLocalEmbeddingRuntimeBlocker();
if (runtimeBlocker) {
throw new Error(runtimeBlocker);
}
// Return existing instance if available
if (embedderInstance) {
return embedderInstance;
@@ -166,6 +177,10 @@ export const initEmbedder = async (
initPromise = (async () => {
try {
// Lazy-load transformers.js only after the runtime guard has passed, so
// unsupported platforms never reach the native ONNX import (#1515).
const { pipeline, env } = await import('@huggingface/transformers');
// Configure transformers.js environment
env.allowLocalModels = false;
// Bridge user-controlled env vars to transformers.js: HF_HOME →
@@ -0,0 +1,79 @@
/**
* Local embedding runtime support guard.
*
* The bundled local embedding stack (`@huggingface/transformers` →
* `onnxruntime-node`) only ships native ONNX Runtime bindings for a subset of
* platform/arch pairs. On macOS Intel (`darwin`/`x64`), `onnxruntime-node`
* ships no `bin/napi-v6/darwin/x64/onnxruntime_binding.node`, so *importing*
* transformers.js throws a raw `Cannot find module ...onnxruntime_binding.node`
* before any device/backend selection can run (#1515). `ONNX_WEB_BACKEND=wasm`
* cannot rescue this — the failure is at native-module import time, not backend
* selection (#1516).
*
* This module is intentionally free of any native or transformers.js import (at
* module scope or inside its functions) so it can be consulted *before* the
* dynamic import that would crash. HTTP embedding mode never touches the native
* runtime, so callers in HTTP mode must skip this guard.
*/
/**
* Stable lead line of the macOS-Intel blocker message. Also used to recognise
* the thrown error in the CLI error handler without coupling to the full
* wording (see {@link isLocalEmbeddingRuntimeBlockerMessage}).
*/
const LOCAL_EMBEDDING_BLOCKER_LEAD =
'Local semantic embeddings are unavailable on macOS Intel (darwin/x64).';
export interface LocalEmbeddingRuntimeOptions {
platform?: NodeJS.Platform;
arch?: NodeJS.Architecture;
}
/**
* Return a human-readable explanation when the *local* embedding runtime cannot
* load on this platform, or `null` when local embeddings are expected to work.
*
* Only `darwin`/`x64` is blocked today: it is the one platform/arch pair where
* the bundled `onnxruntime-node` ships no native binding (#1515). Every other
* platform returns `null` and follows the normal device-probe path, so genuine
* ONNX failures on supported platforms are never masked by this message.
*
* Accepts an explicit `{ platform, arch }` for testing; defaults to the current
* process values.
*/
export const getLocalEmbeddingRuntimeBlocker = (
options: LocalEmbeddingRuntimeOptions = {},
): string | null => {
const platform = options.platform ?? process.platform;
const arch = options.arch ?? process.arch;
if (platform === 'darwin' && arch === 'x64') {
return [
LOCAL_EMBEDDING_BLOCKER_LEAD,
'The bundled ONNX Runtime package (onnxruntime-node) does not ship a',
'darwin/x64 native binding, so the local embedding model cannot load here.',
'ONNX_WEB_BACKEND=wasm does not help: the failure happens while importing',
'the native runtime, before any backend can be selected. Forcing',
'GITNEXUS_EMBEDDING_DEVICE=wasm (or cpu) does not help either, for the same reason.',
'',
'Use one of these instead:',
' - Run analyze without --embeddings (all other indexing still works).',
' - Point GITNEXUS_EMBEDDING_URL (with GITNEXUS_EMBEDDING_MODEL) at an',
' OpenAI-compatible /v1/embeddings endpoint to embed over HTTP.',
' - Run GitNexus on Linux or in Docker, where the native binding ships.',
' - Run GitNexus on Apple Silicon (darwin/arm64), which ships a binding.',
' - Use a future GitNexus build that restores darwin/x64 ONNX support.',
].join('\n');
}
return null;
};
/**
* True when `message` is the macOS-Intel local-embedding blocker produced by
* {@link getLocalEmbeddingRuntimeBlocker}. Lets the CLI surface a clean,
* actionable message instead of a raw stack trace, without coupling to the
* full wording.
*/
export const isLocalEmbeddingRuntimeBlockerMessage = (message: string): boolean =>
message.includes(LOCAL_EMBEDDING_BLOCKER_LEAD);
@@ -55,6 +55,7 @@ const METHOD_ANNOTATION_TO_HTTP: Record<string, string> = {
interface SpringRouteBinding {
method: string;
path: string;
ownerPrefix?: string;
}
interface SpringMethodInfo {
@@ -395,6 +396,25 @@ function joinPath(prefix: string, methodPath: string): string {
return `/${cleanPrefix}/${cleanSub}`;
}
function joinInheritedSpringPath(
controllerPrefix: string,
inheritedPath: string,
inheritedOwnerPrefix = '',
): string {
const joined = joinPath(controllerPrefix, inheritedPath);
const cleanPrefix = controllerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
const cleanOwnerPrefix = inheritedOwnerPrefix.replace(/^\/+/, '').replace(/\/+$/, '');
const cleanInherited = inheritedPath.replace(/^\/+/, '');
if (!cleanPrefix) return joined;
if (
cleanPrefix === cleanOwnerPrefix &&
(cleanInherited === cleanPrefix || cleanInherited.startsWith(`${cleanPrefix}/`))
) {
return `/${cleanInherited}`;
}
return joined;
}
function getNodeName(node: Parser.SyntaxNode): string | null {
return node.childForFieldName('name')?.text ?? null;
}
@@ -634,6 +654,7 @@ function scanSpringProject(files: readonly HttpScanInput[]): HttpFileDetections[
const routes = method.routes.map((route) => ({
method: route.method,
path: type.classPrefix ? joinPath(type.classPrefix, route.path) : route.path,
ownerPrefix: type.classPrefix,
}));
if (routes.length > 0) methodMap.set(method.name, routes);
}
@@ -651,7 +672,7 @@ function scanSpringProject(files: readonly HttpScanInput[]): HttpFileDetections[
const routes = routeMap.get(method.name) ?? [];
return routes.map((route) => ({
method: route.method,
path: joinPath(type.classPrefix, route.path),
path: joinInheritedSpringPath(type.classPrefix, route.path, route.ownerPrefix),
}));
});
-77
View File
@@ -1,77 +0,0 @@
import { LRUCache } from 'lru-cache';
import Parser from 'tree-sitter';
import { logger } from '../logger.js';
/**
* Minimal structural shape consumers need when reading Trees back
* through a phase-dependency boundary. Declared here so phases that
* receive ASTCache via `getPhaseOutput<...>` don't hand-roll their
* own inline structural types that silently drift when ASTCache's
* contract changes.
*
* Typed as `unknown` at the Tree boundary because consumers on the
* other side of the phase-output map don't share tree-sitter's type
* graph (e.g. COBOL's standalone processor).
*/
export interface ASTCacheReader {
get(filePath: string): unknown;
clear(): void;
}
// Define the interface for the Cache
export interface ASTCache extends ASTCacheReader {
get: (filePath: string) => Parser.Tree | undefined;
set: (filePath: string, tree: Parser.Tree) => void;
clear: () => void;
stats: () => { size: number; maxSize: number };
}
export const createASTCache = (maxSize: number = 50): ASTCache => {
const effectiveMax = Math.max(maxSize, 1);
// Initialize the cache with a 'dispose' handler
// This is the magic: When an item is evicted (dropped), this runs automatically.
const cache = new LRUCache<string, Parser.Tree>({
max: effectiveMax,
dispose: (tree) => {
try {
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter
// trees are GC-managed and .delete is absent (no-op here).
//
// Single-owner invariant (load-bearing under WASM): a given
// Parser.Tree reference must live in AT MOST ONE ASTCache
// that disposes. The parse-phase chunk-local cache clears
// between chunks; the cross-phase `scopeTreeCache` (also an
// ASTCache today) holds the same Tree by reference. Under
// native tree-sitter this is benign (dispose is a no-op).
// If/when GitNexus adopts web-tree-sitter for sequential
// parsing, the cross-phase cache must either (a) skip
// writing Trees that are already owned by a disposing cache,
// or (b) use tree.copy() per entry. Failing to pick one
// will hand freed memory to scope-resolution.
(tree as unknown as { delete?: () => void }).delete?.();
} catch (e) {
logger.warn({ e }, 'Failed to delete tree from WASM memory');
}
},
});
return {
get: (filePath: string) => {
const tree = cache.get(filePath);
return tree; // Returns undefined if not found
},
set: (filePath: string, tree: Parser.Tree) => {
cache.set(filePath, tree);
},
clear: () => {
cache.clear();
},
stats: () => ({
size: cache.size,
maxSize: effectiveMax,
}),
};
};
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -7,8 +7,8 @@
* call-processor so that the classification logic lives in one place.
*
* Heritage (mixins: include/extend/prepend) was previously routed here
* but is now handled by heritageExtractor.extractFromCall before the
* call router runs. The router still returns 'skip' for these calls.
* but is now emitted by the scope-resolution pipeline. The router still
* returns 'skip' for these calls so they don't become spurious call edges.
*
* NOTE: This file is intentionally duplicated in gitnexus-web/ because the
* two packages have separate build targets (Node native vs WASM/browser).
@@ -24,9 +24,10 @@ export type CallRoutingResult = RubyCallRouting | null;
/**
* Per-language call router.
* IMPORTANT: Call-routed imports bypass preprocessImportPath(), so any router that
* returns an importPath MUST validate it independently (length cap, control-char
* rejection). See routeRubyCall for the reference implementation.
* IMPORTANT: Call-routed imports are NOT sanitized by the standard import-path
* cleaner, so any router that returns an importPath MUST validate it
* independently (length cap, control-char rejection). See routeRubyCall for the
* reference implementation.
*/
export type CallRouter = (calledName: string, callNode: SyntaxNode) => CallRoutingResult;
@@ -82,10 +83,10 @@ export function routeRubyCall(calledName: string, callNode: SyntaxNode): RubyCal
return { kind: 'import', importPath, isRelative };
}
// ── include / extend / prepend — heritage (now handled by heritageExtractor) ─
// Call-based heritage is intercepted by heritageExtractor.extractFromCall
// before the call router runs. Return SKIP_RESULT so these calls don't
// fall through to normal call processing.
// ── include / extend / prepend — heritage (emitted by scope-resolution) ─
// Call-based heritage (Ruby mixins) is emitted by the scope-resolution
// pipeline. Return SKIP_RESULT so these calls don't fall through to normal
// call processing and become spurious call edges.
if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
return SKIP_RESULT;
}
-97
View File
@@ -78,100 +78,3 @@ export interface CallExtractionConfig {
*/
typeAsReceiverHeuristic?: boolean;
}
// ---------------------------------------------------------------------------
// Call-resolution DAG types
// ---------------------------------------------------------------------------
//
// The call-resolution pipeline is a typed DAG:
//
// extract-call ──▶ classify-form ──▶ infer-receiver ──▶ select-dispatch ──▶ resolve-target ──▶ emit-edge
//
// Provider hooks plug in at infer-receiver and select-dispatch; shared stages
// stay language-agnostic. Stages 1-2 run in the parse worker; stages 3-6 run
// on the main thread. DAG-internal types below are main-thread-only and never
// serialize to the graph.
/**
* DAG stage 3 output: call record with receiver type and source discriminant.
*
* `receiverTypeName` is resolved via TypeEnv → constructor-map → class-as-receiver →
* mixed-chain, or synthesized by `inferImplicitReceiver`. `receiverSource` tags
* which path won and drives MRO strategy selection in stage 4.
*
* Invariants:
* - `receiverSource` MUST match how `receiverTypeName` was resolved; every
* discriminant must have a live reader and writer.
* - `hint` is opaque to shared stages; only the same provider's `selectDispatch` reads it.
*
* @see language-provider.ts § inferImplicitReceiver, selectDispatch
*/
export interface ReceiverEnriched {
readonly calledName: string;
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
readonly receiverName: string | undefined;
readonly receiverTypeName: string | undefined;
readonly receiverSource:
| 'none'
| 'typed-binding'
| 'constructor-map'
| 'class-as-receiver'
| 'mixed-chain'
| 'implicit-self';
/** Free-form hint from the provider hook; opaque to shared stages. */
readonly hint?: string;
}
/**
* Provider hook output for `LanguageProvider.inferImplicitReceiver` (DAG stage 3).
*
* Overlay applied to `ReceiverEnriched` when an implicit receiver is synthesized.
* Ruby example: bare `serialize` inside `Account#call_serialize` →
* `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
* receiverSource: 'implicit-self', hint: 'instance' }`
*
* Invariants:
* - `receiverSource` is always `'implicit-self'` — the only variant this type produces.
* - `callForm` is always `'member'` — the rewrite converts bare-call to method invocation.
* - `hint` is opaque to shared stages; consumed by the same language's `selectDispatch`.
*/
export interface ImplicitReceiverOverride {
readonly callForm: 'free' | 'member' | 'constructor';
readonly receiverName: string;
readonly receiverTypeName: string;
readonly receiverSource: Extract<ReceiverEnriched['receiverSource'], 'implicit-self'>;
/** Free-form language tag (e.g. Ruby sets 'singleton' for `def self.foo`
* method bodies). Consumed by the same language's `selectDispatch` hook. */
readonly hint?: string;
}
/**
* DAG stage 4 output: dispatch strategy for resolving the target method.
*
* Encodes which resolver branch to try first and an optional fallback.
* Stage 5 delegates to `resolveMemberCall`, `resolveFreeCall`, or
* `resolveStaticCall` based on `primary`.
*
* - `primary`: `'owner-scoped'` = MRO walk, `'free'` = arity-tiered global lookup,
* `'constructor'` = type instantiation.
* - `fallback`: Only `'free-arity-narrowed'` exists; used by Ruby implicit-self
* to degrade to arity-tiered free lookup when the MRO walk misses.
* - `ancestryView`: Ruby `'ruby-mixin'` only. `'singleton'` walks extend providers
* only; a miss NEVER falls through to file-scoped lookup (enforced in
* resolveCallTarget). `'instance'` is the default.
*
* Common patterns:
* - `{primary: 'constructor'}` — constructor call
* - `{primary: 'owner-scoped'}` — member call with known type
* - `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}` — Ruby implicit-self
* - `{primary: 'owner-scoped', ancestryView: 'singleton'}` — Ruby class-method call
*
* @see language-provider.ts § selectDispatch
* @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
*/
export interface DispatchDecision {
readonly primary: 'owner-scoped' | 'free' | 'constructor';
readonly fallback?: 'free-arity-narrowed';
readonly ancestryView?: 'instance' | 'singleton';
readonly hint?: string;
}
@@ -45,10 +45,33 @@ export const cClassConfig: ClassExtractionConfig = {
export const cppClassConfig: ClassExtractionConfig = {
language: SupportedLanguages.CPlusPlus,
typeDeclarationNodes: ['class_specifier', 'struct_specifier', 'enum_specifier'],
ancestorScopeNodeTypes: ['namespace_definition', 'class_specifier', 'struct_specifier'],
// #1995: `union_specifier` is included so a type nested in a NAMED union
// (`union U1 { struct Inner {...} }`) qualifies as `U1.Inner`. Anonymous unions
// have no `name` child → extractScopeSegmentsFromNode returns [] → they correctly
// contribute nothing (members inject into the enclosing scope). C uses the
// separate cClassConfig (no qualifiedNodeId), so it is intentionally untouched.
ancestorScopeNodeTypes: [
'namespace_definition',
'class_specifier',
'struct_specifier',
'union_specifier',
],
// #1978: key nested-type nodes by their fully-qualified path (Outer.Inner) so
// same-tail nested types in one TU stay distinct instead of silently merging.
qualifiedNodeId: true,
// #1995: anonymous namespaces have no `name` child, so the generic scope walker
// drops them (empty segment) and two `namespace { struct Inner {} }` blocks in one
// TU collapse onto a single `Inner` node. Give each anonymous namespace_definition
// a deterministic per-block discriminator (its start byte — stable across the
// sequential and worker full-file parses) so the nested types stay distinct.
// Returning `undefined` for every other scope — named namespaces (incl. `inline
// namespace`), classes, structs, named unions — falls through to the default
// name-based extraction, leaving them unchanged. Anonymous UNIONS are not matched
// here (members inject into the enclosing scope), so they keep yielding [].
extractScopeSegments: (node) =>
node.type === 'namespace_definition' && !node.childForFieldName?.('name')
? [`@anon${node.startIndex}`]
: undefined,
extractName: (node) => {
const nameNode = node.childForFieldName?.('name');
if (!nameNode) return undefined;
@@ -164,6 +164,14 @@ export function createClassExtractor(config: ClassExtractionConfig): ClassExtrac
return extract(node, { name: simpleName })?.qualifiedName ?? null;
},
// #1991: qualify a non-typeDeclaration scope node (e.g. a Ruby `module` → Trait)
// by the same ancestor-scope walk the node-id path uses, so two same-tail nested
// mixin modules stay distinct. extract()/extractQualifiedName cannot be reused —
// they bail on non-typeDeclarations (a module is not in typeDeclarationNodes).
qualifyScopeName(node: SyntaxNode, simpleName: string): string {
return buildQualifiedName(node, simpleName);
},
shouldSkipClassCapture(context): boolean {
return config.shouldSkipClassCapture?.(context) ?? false;
},
@@ -44,6 +44,14 @@ export interface ClassExtractor {
},
): ExtractedClassSymbol | null;
extractQualifiedName(node: SyntaxNode, simpleName: string): string | null;
/**
* #1991: qualify a scope-defining node that maps to a class-like registry label
* (e.g. a Ruby `module` → Trait) but is NOT a typeDeclaration, so it cannot go
* through extract()/extractQualifiedName (which bail on non-typeDeclarations).
* Walks the same ancestor scopes as the node-id path. Optional — only providers
* that materialize such nodes implement it.
*/
qualifyScopeName?(node: SyntaxNode, simpleName: string): string;
shouldSkipClassCapture?(
context: ClassCaptureContext & { nodeLabel: ClassLikeNodeLabel },
): boolean;
@@ -267,9 +267,14 @@ function buildRelationship(
/**
* Map a `Reference.kind` to an existing `RelationshipType`. Read/write
* both fold into `ACCESSES`; `type-reference` + `import-use` both fold
* into `USES`. This keeps the graph schema additive — no new
* both fold into `ACCESSES`; `type-reference`, `import-use`, and `macro`
* all fold into `USES`. This keeps the graph schema additive — no new
* RelationshipType values are introduced by this module.
*
* `macro` folds into `USES` (not `CALLS`) deliberately: a macro
* invocation targets a `Macro` node, not a callable function, so keeping
* it out of the `CALLS` keyspace preserves the invariant that `CALLS`
* edges denote function/method dispatch.
*/
function mapKindToType(kind: Reference['kind']): RelationshipType {
switch (kind) {
@@ -282,6 +287,7 @@ function mapKindToType(kind: Reference['kind']): RelationshipType {
return 'INHERITS';
case 'type-reference':
case 'import-use':
case 'macro':
return 'USES';
}
}
@@ -4,7 +4,8 @@
* Determines whether a symbol (function, class, etc.) is exported/public
* in its language. This is a pure function — safe for use in worker threads.
*
* Shared between parse-worker.ts (worker pool) and parsing-processor.ts (sequential fallback).
* Used by the language providers during worker parsing (parse-worker.ts) — the
* sole parse path. (Sequential parsing was removed.)
*/
import { findSiblingChild, type SyntaxNode } from './utils/ast-helpers.js';
@@ -2,15 +2,59 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { FieldExtractionConfig } from '../generic.js';
import type { FieldVisibility } from '../../field-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import { hasKeyword } from './helpers.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
/**
* Dart field extraction config.
*
* Dart class fields appear as declaration nodes inside class_body.
* Dart class fields appear as `declaration` nodes inside `class_body`.
* Two shapes carry the field name(s):
* - instance / plain fields → `initialized_identifier_list`
* (`int z = 0;`, `int a = 1, b = 2;`)
* - `static const` / `static final` / `const` fields → `static_final_declaration_list`
* (`static const a = 1;`, `static final String b = 'x', c = 'y';`)
* Both shapes may declare SEVERAL fields in one declaration, so name extraction
* is multi-name (`extractNames`). The structure query (`DART_QUERIES`) emits one
* `@definition.property` per name for both shapes; this config enriches each.
*
* Visibility is convention-based: underscore prefix = private.
*/
/** All field names declared by a `declaration` node, across both Dart shapes. */
function extractDartFieldNames(node: SyntaxNode): string[] {
const names: string[] = [];
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
// instance / plain fields: initialized_identifier_list > initialized_identifier > identifier
if (child.type === 'initialized_identifier_list') {
for (let j = 0; j < child.namedChildCount; j++) {
const init = child.namedChild(j);
if (init?.type === 'initialized_identifier') {
const ident = init.firstNamedChild;
if (ident?.type === 'identifier') names.push(ident.text);
}
}
}
// static const / final fields: static_final_declaration_list > static_final_declaration > identifier
if (child.type === 'static_final_declaration_list') {
for (let j = 0; j < child.namedChildCount; j++) {
const decl = child.namedChild(j);
if (decl?.type === 'static_final_declaration') {
const ident = decl.firstNamedChild;
if (ident?.type === 'identifier') names.push(ident.text);
}
}
}
}
return names;
}
export const dartConfig: FieldExtractionConfig = {
language: SupportedLanguages.Dart,
typeDeclarationNodes: ['class_definition'],
@@ -18,31 +62,20 @@ export const dartConfig: FieldExtractionConfig = {
bodyNodeTypes: ['class_body'],
defaultVisibility: 'public',
// One AST `declaration` node may declare several fields (`int a, b;`,
// `static final String b = 'x', c = 'y';`), so use the multi-name path.
extractName(node) {
// declaration > initialized_identifier_list > initialized_identifier > identifier
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'initialized_identifier_list') {
for (let j = 0; j < child.namedChildCount; j++) {
const init = child.namedChild(j);
if (init?.type === 'initialized_identifier') {
const ident = init.firstNamedChild;
if (ident?.type === 'identifier') return ident.text;
}
}
}
if (child?.type === 'initialized_identifier') {
const ident = child.firstNamedChild;
if (ident?.type === 'identifier') return ident.text;
}
}
// fallback: look for direct identifier
const name = node.childForFieldName('name');
return name?.text;
return extractDartFieldNames(node)[0];
},
extractNames(node) {
return extractDartFieldNames(node);
},
extractType(node) {
// declaration > type_identifier (first named child usually)
// declaration > type_identifier (the type annotation, present for both the
// instance-field shape and `static final String b = …`). `static const a = 1;`
// has no annotation → undefined (untyped).
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child && (child.type === 'type_identifier' || child.type === 'function_type')) {
@@ -52,22 +85,16 @@ export const dartConfig: FieldExtractionConfig = {
return undefined;
},
extractVisibility(node) {
// Dart uses _ prefix for private
// Walk to find the identifier name
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'initialized_identifier_list') {
for (let j = 0; j < child.namedChildCount; j++) {
const init = child.namedChild(j);
if (init?.type === 'initialized_identifier') {
const ident = init.firstNamedChild;
if (ident?.text?.startsWith('_')) return 'private';
}
}
}
}
return 'public';
// Per-name: Dart convention is underscore-prefixed = private. A single
// declaration can mix visibilities (`static const _p = 1, q = 2;`), so the
// decision is keyed on the individual field name.
extractVisibilityForName(_node, name): FieldVisibility {
return name.startsWith('_') ? 'private' : 'public';
},
extractVisibility(node): FieldVisibility {
const first = extractDartFieldNames(node)[0];
return first?.startsWith('_') ? 'private' : 'public';
},
isStatic(node) {
@@ -75,6 +102,8 @@ export const dartConfig: FieldExtractionConfig = {
},
isReadonly(node) {
// `final` / `const` (both `final_builtin`/`const_builtin` nodes whose text
// is `final`/`const`) are read-only.
return hasKeyword(node, 'final') || hasKeyword(node, 'const');
},
};
@@ -3,6 +3,8 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { FieldExtractionConfig } from '../generic.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { FieldVisibility } from '../../field-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Go field extraction config.
@@ -13,14 +15,52 @@ import { extractSimpleTypeName } from '../../type-extractors/shared.js';
* Visibility in Go is based on the first character: uppercase = exported (public),
* lowercase = unexported (package).
*/
function goVisibilityForName(name: string): FieldVisibility {
const first = name.charAt(0);
return first === first.toUpperCase() && first !== first.toLowerCase() ? 'public' : 'package';
}
function extractGoFieldNames(node: SyntaxNode): string[] {
const names: string[] = [];
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'field_identifier') names.push(child.text);
}
return names;
}
export const goConfig: FieldExtractionConfig = {
language: SupportedLanguages.Go,
typeDeclarationNodes: ['type_declaration'],
typeDeclarationNodes: ['type_declaration', 'struct_type'],
fieldNodeTypes: ['field_declaration'],
bodyNodeTypes: ['field_declaration_list'],
defaultVisibility: 'package',
extractOwnerName(node) {
if (node.type === 'struct_type') {
return node.parent?.type === 'type_spec'
? node.parent.childForFieldName('name')?.text
: undefined;
}
const typeSpec = node.namedChildren.find((child) => child.type === 'type_spec');
return typeSpec?.childForFieldName('name')?.text;
},
findBodyNodes(node) {
if (node.type === 'struct_type') {
const body = node.namedChildren.find((child) => child.type === 'field_declaration_list');
return body ? [body] : [];
}
const typeSpec = node.namedChildren.find((child) => child.type === 'type_spec');
const typeNode = typeSpec?.childForFieldName('type');
const body = typeNode?.namedChildren.find((child) => child.type === 'field_declaration_list');
return body ? [body] : [];
},
extractName(node) {
const firstName = extractGoFieldNames(node)[0];
if (firstName) return firstName;
// field_declaration > name:(field_identifier)
const name = node.childForFieldName('name');
if (name) return name.text;
@@ -32,6 +72,8 @@ export const goConfig: FieldExtractionConfig = {
return undefined;
},
extractNames: extractGoFieldNames,
extractType(node) {
// field_declaration > type:(type_identifier | pointer_type | ...)
const typeNode = node.childForFieldName('type');
@@ -54,6 +96,10 @@ export const goConfig: FieldExtractionConfig = {
return 'package';
},
extractVisibilityForName(_node, name) {
return goVisibilityForName(name);
},
isStatic(_node) {
return false; // Go has no static fields
},
@@ -5,6 +5,7 @@ import type { FieldExtractionConfig } from '../generic.js';
import { findVisibility, hasKeyword, hasModifier, typeFromField } from './helpers.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { FieldVisibility } from '../../field-types.js';
import type { SyntaxNode } from '../../utils/ast-helpers.js';
// ---------------------------------------------------------------------------
// Java
@@ -73,13 +74,49 @@ export const javaConfig: FieldExtractionConfig = {
const KOTLIN_VIS = new Set<FieldVisibility>(['public', 'private', 'protected', 'internal']);
/** A property_declaration is a companion-object member when its nearest
* class-body ancestor is the body of a companion_object (F52, issue #1919).
* Companion members are addressed statically through the enclosing class
* (`C.TAG`), so they are marked static. */
function isInsideKotlinCompanion(node: SyntaxNode): boolean {
for (let cur = node.parent; cur !== null; cur = cur.parent) {
if (cur.type === 'class_body') return cur.parent?.type === 'companion_object';
if (cur.type === 'companion_object') return true;
}
return false;
}
export const kotlinConfig: FieldExtractionConfig = {
language: SupportedLanguages.Kotlin,
typeDeclarationNodes: ['class_declaration', 'object_declaration'],
// F52: include companion_object so a companion property's innermost
// class-container owner (findEnclosingClassNode returns the companion_object)
// is recognized as a type declaration and its nested class_body is walked.
// The structure query already creates the Property node and owns it on the
// ENCLOSING class for anonymous companions / on the named companion Class —
// this entry only drives field-metadata enrichment, so it does NOT change
// ownership or emit a second node (no double-count).
typeDeclarationNodes: ['class_declaration', 'object_declaration', 'companion_object'],
fieldNodeTypes: ['property_declaration'],
bodyNodeTypes: ['class_body'],
defaultVisibility: 'public',
// F52: an anonymous `companion object { ... }` has no name child, so the
// generic factory's `childForFieldName('name')` owner lookup is empty and
// `extract()` would bail before walking the body. Supply a stable owner
// name (the named companion's identifier, else "Companion") so the body IS
// walked; the resulting FieldInfo map is keyed by field NAME only, so the
// owner name does not affect which Property node gets enriched.
extractOwnerName(node) {
const typeIdentifierText = node.namedChildren.find((c) => c.type === 'type_identifier')?.text;
if (node.type === 'companion_object') {
// Anonymous companions have no type_identifier — fall back to "Companion".
return typeIdentifierText ?? 'Companion';
}
const name = node.childForFieldName('name');
if (name) return name.text;
return typeIdentifierText;
},
extractName(node) {
// property_declaration > variable_declaration > simple_identifier
for (let i = 0; i < node.namedChildCount; i++) {
@@ -124,9 +161,11 @@ export const kotlinConfig: FieldExtractionConfig = {
return findVisibility(node, KOTLIN_VIS, 'public', 'modifiers');
},
isStatic(_node) {
// Kotlin doesn't have static; companion object members are handled separately
return false;
isStatic(node) {
// Kotlin has no `static`, but companion-object members are accessed
// statically through the enclosing class (`C.TAG`) — mark them static
// so the field metadata reflects that (F52).
return isInsideKotlinCompanion(node);
},
isReadonly(node) {
@@ -2,7 +2,7 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { FieldExtractionConfig } from '../generic.js';
import { hasKeyword, findVisibility } from './helpers.js';
import { hasKeyword, hasModifier, findVisibility } from './helpers.js';
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
import type { FieldVisibility } from '../../field-types.js';
@@ -17,18 +17,33 @@ const SWIFT_VIS = new Set<FieldVisibility>([
/**
* Swift field extraction config.
*
* Handles property_declaration inside class_body / protocol_body.
* Handles property_declaration inside class_body / protocol_body and
* protocol_property_declaration inside protocol_body (F75 — protocol property
* requirements like "var title: String { get }").
*
* tree-sitter-swift uses property_declaration for stored/computed properties.
* A protocol property requirement parses to its own node type,
* protocol_property_declaration, whose name lives in a "name:" pattern field
* (pattern > value_binding_pattern + simple_identifier(bound_identifier)), its
* type in a sibling type_annotation, and its "{ get }" / "{ get set }" in a
* protocol_property_requirements child. Note: Swift reuses the "name:" field
* across many positions (func name, every parameter label, parameter/return
* type), so the name is synthesized from the simple_identifier inside the
* pattern rather than read blindly off "name:".
*/
export const swiftConfig: FieldExtractionConfig = {
language: SupportedLanguages.Swift,
typeDeclarationNodes: ['class_declaration', 'protocol_declaration'],
fieldNodeTypes: ['property_declaration'],
fieldNodeTypes: ['property_declaration', 'protocol_property_declaration'],
bodyNodeTypes: ['class_body', 'protocol_body'],
defaultVisibility: 'internal',
extractName(node) {
// property_declaration > pattern > simple_identifier
// property_declaration > pattern > simple_identifier, and
// protocol_property_declaration > name: (pattern ... simple_identifier).
// For protocol_property_declaration the pattern wraps a leading
// value_binding_pattern ("var") plus the simple_identifier — the loop
// below skips the binding keyword and returns the identifier.
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child?.type === 'pattern') {
@@ -62,7 +77,19 @@ export const swiftConfig: FieldExtractionConfig = {
},
isStatic(node) {
return hasKeyword(node, 'static') || hasKeyword(node, 'class');
// `static`/`class` (type-level) modifiers live inside a `modifiers`
// wrapper for both property_declaration and protocol_property_declaration
// (e.g. `static var shared: P { get }`), so check the wrapper too.
// `hasKeyword` compares each direct child by `.text` equality: it matches a
// single-modifier wrapper (`modifiers.text === 'static'`) but fails for a
// multi-modifier wrapper (`private static` → `modifiers.text === 'private static'`),
// which `hasModifier` handles by descending into the wrapper's children.
return (
hasKeyword(node, 'static') ||
hasKeyword(node, 'class') ||
hasModifier(node, 'modifiers', 'static') ||
hasModifier(node, 'modifiers', 'class')
);
},
isReadonly(node) {
@@ -33,6 +33,10 @@ export interface FieldExtractionConfig {
bodyNodeTypes: string[];
/** Default visibility when no modifier is present */
defaultVisibility: FieldVisibility;
/** Extract owner type name from a type declaration node. */
extractOwnerName?: (node: SyntaxNode) => string | undefined;
/** Find body nodes inside a type declaration node. */
findBodyNodes?: (node: SyntaxNode) => SyntaxNode[];
/**
* Extract field name from a field declaration node.
* Use this for nodes that declare exactly one field.
@@ -49,6 +53,8 @@ export interface FieldExtractionConfig {
extractType: (node: SyntaxNode) => string | undefined;
/** Extract visibility from a field declaration node */
extractVisibility: (node: SyntaxNode) => FieldVisibility;
/** Extract visibility for one field name from a multi-name declaration. */
extractVisibilityForName?: (node: SyntaxNode, name: string) => FieldVisibility;
/** Check if a field is static */
isStatic: (node: SyntaxNode) => boolean;
/** Check if a field is readonly/final/const */
@@ -84,10 +90,9 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
extract(node: SyntaxNode, context: FieldExtractorContext): ExtractedFields | null {
if (!this.isTypeDeclaration(node)) return null;
const nameNode = node.childForFieldName('name');
if (!nameNode) return null;
const ownerFqn = config.extractOwnerName?.(node) ?? node.childForFieldName('name')?.text;
if (!ownerFqn) return null;
const ownerFqn = nameNode.text;
const fields: FieldInfo[] = [];
// Find body container(s)
@@ -110,6 +115,8 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
// ------------------------------------------------------------------
private findBodies(node: SyntaxNode): SyntaxNode[] {
if (config.findBodyNodes) return config.findBodyNodes(node);
const result: SyntaxNode[] = [];
// Try named 'body' field first
const bodyField = node.childForFieldName('body');
@@ -179,7 +186,7 @@ export function createFieldExtractor(config: FieldExtractionConfig): FieldExtrac
return {
name,
type,
visibility: config.extractVisibility(node),
visibility: config.extractVisibilityForName?.(node, name) ?? config.extractVisibility(node),
isStatic: config.isStatic(node),
isReadonly: config.isReadonly(node),
sourceFile: context.filePath,
@@ -6,10 +6,6 @@ import { glob } from 'glob';
import { createIgnoreFilter } from '../../config/ignore-service.js';
import { logger } from '../logger.js';
export interface FileEntry {
path: string;
content: string;
}
/** Lightweight entry — path + size from stat, no content in memory */
export interface ScannedFile {
@@ -153,21 +149,3 @@ export const readFileContents = async (
return contents;
};
/**
* Legacy API — scans and reads everything into memory.
* Used by sequential fallback path only.
*/
export const walkRepository = async (
repoPath: string,
onProgress?: (current: number, total: number, filePath: string) => void,
): Promise<FileEntry[]> => {
const scanned = await walkRepositoryPaths(repoPath, onProgress);
const contents = await readFileContents(
repoPath,
scanned.map((f) => f.path),
);
return scanned
.filter((f) => contents.has(f.path))
.map((f) => ({ path: f.path, content: contents.get(f.path)! }));
};
@@ -51,6 +51,8 @@ import {
finalize,
} from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from './model/scope-resolution-indexes.js';
import { parseTruthyEnv } from './utils/env.js';
import { TransitionalScopeTree } from '../../storage/scope-index-store.js';
// ─── Public entry point ─────────────────────────────────────────────────────
@@ -114,14 +116,20 @@ export function finalizeScopeModel(
moduleEntries.push({ filePath: file.filePath, moduleScopeId: file.moduleScope });
}
const scopeTree = buildScopeTree(allScopes);
// Out-of-core scope index: when enabled, build a TransitionalScopeTree
// (validated + fully resident now; sealed to disk by run.ts just before emit so
// the heavy Scope.bindings payload is reclaimed). Default off → the in-heap
// buildScopeTree result exactly, byte-identical.
const scopeTree = parseTruthyEnv(process.env.GITNEXUS_DISK_SCOPE_INDEX)
? new TransitionalScopeTree(allScopes)
: buildScopeTree(allScopes);
const defs = buildDefIndex(allDefs);
const qualifiedNames = buildQualifiedNameIndex(allDefs);
const moduleScopes = buildModuleScopeIndex(moduleEntries);
// ── Step 3: MethodDispatchIndex. Today we lack per-language MRO
// strategies wired into this orchestrator (that belongs with the
// HeritageMap bridge, a separate piece of work). Ship an EMPTY index
// MRO bridge, a separate piece of work). Ship an EMPTY index
// so the bundle shape is consistent; the callbacks return `[]` for
// every owner and `implementsOf` returns `[]`. Populating this
// properly is tracked alongside the per-language provider hooks.
@@ -1,21 +0,0 @@
// gitnexus/src/core/ingestion/heritage-extractors/configs/cpp.ts
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
/**
* C++ base-class supertype shapes (legacy CPP_QUERIES bank only).
*
* A `base_class_clause` entry can be a bare `type_identifier`, a
* `template_type` (`Base<T>`), or a `qualified_identifier` (`ns::Base`,
* possibly itself wrapping a `template_type`). Entries may be prefixed by an
* `access_specifier`; tree-sitter's bracketed alternation matches the base
* node regardless of the preceding access specifier, so a single pattern
* covers both `: Base` and `: public Base`.
*
* NOTE: the registry/scope-resolution path emits its own normalized
* `@reference.inherits` captures (see languages/cpp/captures.ts); this
* descriptor only repairs the legacy heritage-query bank.
*/
export const cppHeritageShapes: SupertypeShapeDescriptor = {
shapes: ['type_identifier', 'template_type', 'qualified_identifier'],
};
@@ -1,38 +0,0 @@
// gitnexus/src/core/ingestion/heritage-extractors/configs/csharp.ts
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
/**
* C# base-list supertype shapes.
*
* Every `base_list` entry (class/record/struct/interface) is captured as
* `@heritage.extends`; EXTENDS-vs-IMPLEMENTS is decided downstream by
* `resolveExtendsType`, so the query does not pre-split. Entries can be a bare
* `identifier`, a `generic_name` (`IFoo<T>`), a `qualified_name` (`ns.Base`,
* and also `global::System.IDisposable` — a dotted name qualified by an
* `alias_qualified_name` parses as a `qualified_name` whose first part is the
* alias), an `alias_qualified_name` (a *bare* alias-qualified base with no
* dotted suffix: `global::IDisposable`, `MyAlias::Foo`), or a
* `primary_constructor_base_type` (`Base(args)` on a record).
*
* NOTE: `scoped_type` is intentionally NOT listed. Per
* tree-sitter-c-sharp/src/node-types.json, `scoped_type` is a `type` subtype
* that wraps a `ref_type` (the `scoped ref`/`scoped in` parameter modifier);
* it is referenced only by the hidden `type` supertype and never appears as a
* `base_list` entry, so adding it would be a dead, untested shape. The
* alias-qualified base shapes that *do* occur are `qualified_name` (dotted) and
* `alias_qualified_name` (bare) — verified by parsing
* `class A : System.Exception, global::System.IDisposable, MyAlias::Foo {}`.
* `normalizeSupertypeName` collapses `alias_qualified_name` to the simple name
* via its `name` field (an `identifier`). See
* `test/integration/heritage-supertype-shapes.test.ts`.
*/
export const csharpHeritageShapes: SupertypeShapeDescriptor = {
shapes: [
'identifier',
'generic_name',
'qualified_name',
'alias_qualified_name',
'primary_constructor_base_type',
],
};

Some files were not shown because too many files have changed in this diff Show More