The handler was allocating a fresh LocalBackend (open DB connections +
repo refresh) on every request and disposing it in finally. createServer
already owns a shared backend for MCP; route /api/group-status through
that instead so concurrent requests no longer multiply DB handles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add CROSS_REPO_IMPORT to RelationshipType so api.ts cast typechecks.
- Wire actual recursion into findConnections so groupGraph depth > 1
traverses transitive cross-links instead of silently stopping at 1.
- Drop shadowing dynamic imports in groupStatus; use the module-level
fsp/path already in scope.
- Switch import-scanner to fs.promises.readFile so file reads no longer
block the event loop inside the async scanner.
- Extend group_discover MCP schema with repoPaths so the explicit-path
mode is reachable via MCP (was CLI-only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLI:
- Split `auto-discover --repos` into separate `group repos <paths...>` command
Landing page:
- Add Repos/Groups tab bar (Groups tab only shown when groups exist)
- Group cards show name, repo count, last sync time
- Scrollable card with max-h-[80vh] to prevent overflow
Graph visualization:
- File/Folder nodes colored by repo in multi-repo mode
- Hover tooltip shows repo name in parentheses
- RepoLegend in top-left with clickable toggles: amber = highlighted,
grey = dimmed. Deselected repos' nodes AND edges dim to ~12% opacity
File tree sidebar:
- In group mode, files grouped under collapsible repo headers
- Repo headers have amber dot and bold font
218 web tests + 5177 backend tests pass. Zero regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add --repos flag to `group auto-discover` so users can pass explicit
repo paths instead of scanning a directory:
gitnexus group auto-discover --repos /path/to/repo1 /path/to/repo2
Also removes unused buildPackageMap() and findSiblingDependencies()
functions, and cleans up redundant inline comments across extractors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add group_graph and group_discover to GROUP_TOOLS set and update
expected tool count from 16 to 18.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- Add /api/groups, /api/group-graph, /api/group-status endpoints
- /api/group-graph returns merged graph with namespaced node IDs
and synthetic CROSS_REPO_IMPORT edges from contracts.json
Web UI:
- Add "Group" toggle button in Header with group selection dropdown
- Add group mode state (groupMode, activeGroup, connectToGroup)
- Add CROSS_REPO_IMPORT edge type with amber color (#f59e0b)
- Add REPO_COLORS palette for multi-repo visual distinction
- Graph adapter detects multi-repo mode from _repo property or
namespaced IDs, positions each repo's nodes in separate regions
Backend client:
- Add fetchGroups(), fetchGroupGraph(), fetchGroupStatus()
- Add GroupGraphResult and GroupStatus types
Includes 15 web UI tests covering constants, graph adapter multi-repo
behavior, and type correctness. All 213 web tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `gitnexus group graph <name> <symbol>` command and `group_graph`
MCP tool that traverses CrossLinks to find how a symbol connects to
other repos in a group. Returns local context plus remote connections
with their contract metadata.
Searches all repos in the group when --repo is not specified. Supports
--depth (max 2) and --direction (upstream/downstream/both) options.
Includes 6 integration tests with mock GroupToolPort.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `gitnexus group auto-discover [directory]` that scans a parent
directory for indexed repos, reads their package manifests, builds
package mappings, and creates a group with code-level dependency
detection enabled.
Also adds `group_discover` MCP tool for programmatic access and
`groupDiscover()` method on GroupService.
Includes 5 integration tests covering discovery, error handling,
and non-indexed directory filtering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add CodeDepExtractor that detects when repo B imports symbols from
repo A's npm package. Wires into the existing group sync pipeline
using the already-declared `lib` contract type and `shared_libs`
detection flag.
New files:
- manifest-reader.ts: reads package.json for name + dependencies
- import-scanner.ts: scans source for ES/CJS imports matching sibling packages
- code-dep-extractor.ts: ContractExtractor producing lib::{pkg}::{symbol} contracts
Auto-discovers package mappings when config.packages is empty by reading
each repo's package.json name and checking cross-dependencies.
Includes 58 unit tests and 3 integration tests covering all extraction
paths, edge cases, and the full sync → match → CrossLink pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "Web UI (browser-based)" section described an old client-side
architecture. Today gitnexus.vercel.app is a thin frontend that
auto-connects to a local `gitnexus serve` backend — there is no
ZIP drag-and-drop and no fully self-contained mode.
- Drop "No server, no install" claim
- Replace "drag & drop a ZIP" tagline with the actual onboarding step
- Add the missing `gitnexus serve` step to the local-dev block
Closes#1110
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: add platform-aware semantic fallback
Make VECTOR an optional capability so Windows analysis remains stable while semantic embeddings can fall back to exact scan when native vector indexing is unavailable.
Made-with: Cursor
* fix: remove stale vector pool import
Keep the merge with main lint-clean after VECTOR loading moved out of the read pool.
* fix(swift): use official prebuilt parser runtime
Vendor the official tree-sitter-swift 0.7.1 runtime package so Swift parsing works without source-building, while keeping the repo on the current tree-sitter runtime until the broader upgrade is ready. Also preserves Swift resolver correctness for overloaded owned functions and extension-backed type duplicates now that Swift is available by default.
Made-with: Cursor
* fix(swift): move duplicate type ordering into provider
Keep Swift extension candidate ordering behind the LanguageProvider contract and cover the Swift 0.7 init scanner path so parser runtime changes do not leak language-specific logic into shared resolution.
Made-with: Cursor
* fix(swift): address parser runtime review
Add explicit Swift prebuild checks and vendor guidance so parser runtime packaging remains observable and maintainable.
* fix(hooks): ignore global registry during staleness checks
* test(hooks): cover indexed repos under global registry
---------
Co-authored-by: laplace young <yangqk12@whu.edu.cn>
* fix(group): add configurable cross-link path exclusions to reduce false positives
Add matching.exclude_links_paths and matching.exclude_links_param_only_paths
to group.yaml config. These filter out noisy HTTP contracts (health checks,
param-only catch-all routes) from cross-link matching while preserving them
in the contract registry for documentation purposes.
Defaults are empty/false for backward compatibility — no behavior change
unless the operator explicitly configures exclusions.
* fix(group): address review findings — filter unmatched, normalize trailing slash, add tests
- Excluded contracts no longer inflate SyncResult.unmatched (isNoisy guard)
- pathPart in buildNoisyContractFilter strips trailing slashes before comparison
- 8 new unit tests for buildNoisyContractFilter covering all code paths
- Config-parser test asserts defaults for new matching fields
* fix(group): normalize configured exclusion paths and add root-path test
- Strip trailing slashes from configured exclude_links_paths at Set-build
time so root path '/' (which normalizes to '') matches correctly
- Add test: exclude_links_paths: ['/'] suppresses http::GET::/ contracts
- Add new matching fields as commented examples in fixture group.yaml (DoD §2.4)
* docs(group): document exclude_links_paths and exclude_links_param_only_paths config fields
Add JSDoc to MatchingConfig interface, update the microservices guide
YAML example and field notes, and scaffold the new fields (commented out)
in the group create template.
* fix(lbug): bound DuckDB extension install via ExtensionManager (closes#1128)
`gitnexus analyze` could hang indefinitely (60% / 85% on Windows) when
DuckDB's `INSTALL fts` or `INSTALL VECTOR` was unable to reach
`extensions.duckdb.org`. The DuckDB driver's INSTALL is a synchronous
network call, so any blocked egress would block the Node event loop
forever.
Replace the ad-hoc, in-process INSTALL/LOAD scattered across
`lbug-adapter.ts` and `pool-adapter.ts` with a single
`ExtensionManager` that owns the lifecycle of optional DuckDB
extensions:
* `LOAD` is always tried first — per-connection, idempotent, no network.
* If `LOAD` fails and policy permits, INSTALL runs in a short-lived
child Node process bounded by `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS`
(default 15s). The parent loop keeps spinning; on timeout the child is
killed with SIGKILL and the capability is flagged unavailable.
* Capabilities and install attempts are cached per process, so a single
bounded install per extension covers every subsequent call.
Install policy is now an explicit, per-context decision:
* `auto` (default for analyze) — try LOAD, fall back to bounded INSTALL.
* `load-only` — used by `pool-adapter` (serve / MCP read paths) so user
queries never block on a network install.
* `never` — operator escape hatch for offline / airgapped environments.
`createFTSIndex` and `createVectorIndex` now check the boolean return
value before issuing the index DDL, so missing extensions degrade BM25
and semantic search gracefully without ever throwing during analyze.
Tests:
- New unit suite for `ExtensionManager` covering LOAD-first behavior,
all three policies, install caching, observability, and warn dedup.
- Existing vector-extension integration tests pass against the new
boolean return type.
- Existing embedding-pipeline mocks updated to return `true`.
Docs: `gitnexus/README.md` documents `GITNEXUS_LBUG_EXTENSION_INSTALL`
and `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` with examples for
offline and slow-network environments.
Made-with: Cursor
* fix(lbug): move DuckDB extension install child into script
Keep the bounded out-of-process INSTALL behavior, but replace the inline child code with a stable packaged ESM script. This makes the child process directly runnable and gives debuggable stack traces without source-vs-dist branching or a runtime transpiler.
Made-with: Cursor
Avoid remote git/SSH downloads for the Dart grammar during Docker and npm installs by resolving tree-sitter-dart from vendored source and building it during postinstall.
Made-with: Cursor
installOpenCodeSkills() was writing to ~/.config/opencode/skill/gitnexus/
but OpenCode only discovers skills from ~/.config/opencode/skills/*/SKILL.md.
Skills installed by `gitnexus setup` were silently ignored by OpenCode.
- Line 590: path.join(opencodeDir, 'skill') → 'skills'
- Line 587: updated JSDoc comment to match
* fix(serve): serve web UI at root path instead of 404
gitnexus serve returned Cannot GET / because no route handler existed
for the root path. Now serves the built gitnexus-web dist at / with
SPA fallback for client-side routing. Falls back to a helpful landing
page with API links when the web UI hasn't been built yet.
Also updates the build script to build and copy gitnexus-web into
gitnexus/web/ for the published npm package.
* fix(serve): address Copilot review feedback
- Use regex SPA fallback that excludes /api paths (avoids serving
index.html for unknown API routes)
- Add rel="noopener noreferrer" to external link (reverse-tabnabbing)
- Move build "done" log after web UI step
* fix(build): use npm run build for web UI, add npm install guard
The build script ran `npx tsc -b && npx vite build` in gitnexus-web/,
but CI only installs node_modules for gitnexus/ — not gitnexus-web/.
npx then resolved the wrong `tsc` package (a trojan on npm), causing
all CI jobs to fail.
Fix: add an npm install guard when node_modules is missing, and use
`npm run build` (which runs the local typescript) instead of npx.
* feat(serve): styled fallback page, asset 404s, build script safety
- Add landingPageHtml() with gitnexus-web design tokens (void bg,
surface cards, accent color, terminal-style build command block).
- Add resolveWebDistDir() helper with non-ENOENT error logging.
- Register express.static with Cache-Control headers (no-cache HTML,
immutable assets) and SPA fallback route.
- Replace wildcard SPA fallback with regex that excludes /api/* AND
asset-like file extensions (.js, .css, .ico, .woff2, .map, etc.).
- Add ordering comment warning about SPA fallback route placement.
scripts/build.js:
- Change npm install to npm ci.
- Add timeout: 120_000 to all execSync calls.
Test coverage:
- 26 new unit tests for design tokens, terminal block, external links,
SPA regex acceptance/exclusion, cache headers, and fs.access edge
cases.
Closes#1048 (review feedback)
* fix: format, lint, and add GITNEXUS_WEB_DIST env var
- Remove unused fsType import from web-ui-serving.test.ts (lint error)
- Run prettier on fallback-page-screenshot.html and test file
- Add GITNEXUS_WEB_DIST env var as primary override in resolveWebDistDir
- Add tests for env var: prefer when set, fallback when dir missing
* fix: use cross-platform path matching in env var tests
Path.includes('/env/dist') fails on Windows where path.join
produces backslashed paths. Normalize via path.sep replacement
before matching.
* fix(serve): address PR #1048 review findings
- Add uncaughtException/unhandledRejection crash guards to HTTP serve path
- Export SPA_FALLBACK_REGEX so tests use the production constant (no drift)
- Export staticCacheControlSetHeaders so tests verify the real production function
- Add real Express dispatch tests for API 404 and asset 404 isolation
- Delete committed debug artifact fallback-page-screenshot.html
* fix(scope-resolution): allow same-range Module-as-parent for top-level scopes (closes#1086)
When a C# file consists of a single top-level `namespace_declaration` that
ends exactly at EOF (no trailing newline, no leading content outside the
namespace's `{}` body), tree-sitter-c-sharp 0.23.1 reports identical byte
ranges for `compilation_unit` and `namespace_declaration`. Pre-fix the
scope-extractor parent-finder relied on strict containment, so the Module
was popped off the stack and the Namespace ended up with `parent === null`
→ `ScopeTreeInvariantError: non-module-requires-parent` →
`extractParsedFile` swallowed the throw and the whole file was dropped
from the registry-primary path. Cross-file IMPORTS / CALLS edges
originating in or terminating at that file vanished.
Hit on three real-world `*.Designer.cs` files in PersistentWindows
(`HotKeyWindow.Designer.cs`, `LaunchProcess.Designer.cs`,
`DbKeySelect.Designer.cs`) — all have the byte signature
`<BOM><CRLF>namespace ... { ... }<EOF>` (last hex = `... 7D 0D 0A 7D`).
The fix is a single carve-out in the parent-validity contract: a `Module`
may parent a same-range non-`Module` child. The relationship stays
acyclic because the carve-out is direction-asymmetric — only Module-as-
outer parents a same-range non-Module, never the reverse.
Two coordinated changes:
* `gitnexus/src/core/ingestion/scope-extractor.ts` — `pass1BuildScopes`
now consults a new `canParentScope` helper instead of
`rangeStrictlyContains` directly. Sort tie-breaker added so a same-
range Module always sorts before a non-Module candidate, ensuring the
Module lands on the parent-stack first regardless of tree-sitter
capture iteration order.
* `gitnexus-shared/src/scope-resolution/scope-tree.ts` — `buildScopeTree`'s
`parent-must-contain-child` check now uses the same `canParentScope`
carve-out so the validator agrees with the extractor on what a
well-formed parent edge looks like. Error message updated to spell
out the new contract.
`rangeStrictlyContains` keeps its strict semantics in both files —
position-index lookups, hook-side range comparisons, and other call
sites are unchanged.
* `gitnexus/test/fixtures/lang-resolution/csharp-namespace-as-root-no-trailing-newline/`
— minimal regression fixture mirroring the PersistentWindows shape:
both `Models/User.cs` and `App/Program.cs` end exactly on the closing
`}` of their namespace with no trailing newline. The trigger is shape-
driven, not size-driven, so the fixture stays small (~250 bytes total).
* New `csharp.test.ts` describe block: scope extraction completes for
both files, and the cross-file `IMPORTS` edge resolves through the
scope-resolution path with `reason: 'csharp-scope: using'`.
* `scope-tree.test.ts`: replaced the prior "rejects child ranges
identical to the parent" case with three new ones — non-Module parent
still rejected at equal range; Module-as-parent of a same-range non-
Module accepted (the #1086 carve-out); Module-as-parent of another
Module still rejected (the asymmetry guard).
* `npx vitest run test/unit/scope-resolution test/integration/resolvers`
→ 2514 passed / 77 skipped / 0 failed (52 test files).
* `npx tsc --noEmit` clean in both `gitnexus/` and `gitnexus-shared/`.
* End-to-end on PersistentWindows (after rebuilding the Docker image
with this branch): 3 prior `scope extraction failed for *.Designer.cs`
warnings → 0. Pre-fix index numbers will be re-checked here once the
branch is built and indexed; the existing post-#1082 baseline is
1113 nodes / 2987 edges / 39 clusters / 97 flows.
`canParentScope` is language-agnostic. Other languages whose query emits
`(compilation_unit) @scope.module` plus a single same-range top-level
scope can naturally hit the same byte shape on minimal files; this fix
applies to all of them uniformly.
Refs: #1086 (issue with full root-cause analysis + 4-case empirical
repro through `extractParsedFile`).
* refactor(scope-resolution): export canParentScope from gitnexus-shared
Addresses #1087 review (medium): the helper was previously duplicated
byte-for-byte in `scope-extractor.ts` and `scope-tree.ts`. Per DoD
"single source of truth in shared", the contract piece belongs in
gitnexus-shared (Ring 2 SHARED #912) and the consuming layer should
import it. Eliminates the silent-drift surface where a future edit
to one copy would produce extractor/validator disagreement on what
a well-formed parent edge looks like.
Changes:
- gitnexus-shared/src/scope-resolution/scope-tree.ts: add `export`
to `canParentScope`.
- gitnexus-shared/src/index.ts: re-export `canParentScope`.
- gitnexus/src/core/ingestion/scope-extractor.ts: remove the local
`canParentScope` definition (and its now-unused local copy of
`rangeStrictlyContains`), import from `gitnexus-shared`. The local
`rangesEqual` stays — it's still used in capture-anchor logic at
two unrelated sites.
Validation (per DoD §4.4 — both CLI and web consumers verified):
- npx tsc --noEmit clean in gitnexus/ and gitnexus-shared/
- cd gitnexus-web && npx tsc -b --noEmit clean
- gitnexus-shared `npm run build` clean
- Targeted: vitest run test/unit/scope-resolution test/integration/resolvers
→ 2522 passed / 0 failed / 77 skipped (54 files)
- Full suite: vitest run → 7238 passed / 1 failed / 97 skipped.
The single failure is `test/unit/ignore-service.test.ts > warns
on EACCES but does not throw`, which cannot run when uid=0 (root
bypasses POSIX permission checks). Pre-existing on this branch
before the refactor; unrelated to scope-resolution.
The `loadIgnoreRules — error handling > warns on EACCES but does not
throw` test relies on `chmod 000` denying read access to a temporary
.gitignore file. On Linux, root bypasses POSIX read-permission checks,
so chmod 000 does NOT trigger EACCES under uid=0 — fs.readFile reads
the file anyway and loadIgnoreRules returns parsed rules instead of
the `null` the test expects.
Symptom under root: assertion fails with `Ignore { _rules: [...] }
to be null`, surfaced as a single test failure in any privileged
test environment (rootful Docker container, CI runners configured to
run tests as root, etc.).
Fix: extend the existing `skipIf(process.platform === 'win32')` guard
with `process.getuid?.() === 0`. The non-root code path still
exercises the real EACCES branch — root just can't reproduce the
failure mode the test asserts on, so skipping there is the correct
posture (matches the win32 skip's reasoning: the OS-level mechanism
the test depends on isn't available there).
Optional chaining (`getuid?.()`) keeps Windows compatibility — Node
on Windows doesn't expose `process.getuid` at all.
The csharp-large-cache-miss-resolution fixture added in #1082 reproduces
the freeze contract failure via tree-sitter cache-miss reparse on >32 KB
files. This adds a complementary trigger for the same root cause that
does not depend on file size: a small-file pair where the importer
locally declares a class with the same simple name as a sibling reached
through `using`.
Pre-#1082 path: scope-extractor pre-populates (and freezes) `User` in
the importer's Module bindings, then populateCsharpNamespaceSiblings'
namespace-import loop calls push() on the frozen array and throws
"Cannot add property N, object is not extensible", aborting the whole
scopeResolution phase.
Post-#1082 the augmentation channel keeps both bindings visible; the
local `Collision.App.User` shadows the namespace-imported one per
origin precedence, so `Program.Run -> new User()` resolves to the
local class.
Three assertions:
- scopeResolution completes (no throw on the colliding bucket).
- both `User` declarations are detected across the two namespaces.
- `Program.Run -> User` constructor edge points at App/Program.cs
(not Models/User.cs), verifying origin:local shadows origin:namespace.
Verified: full csharp.test.ts suite green (207/207). tsc --noEmit clean.
Refs: #1066, #1082, #1083 (closed as superseded).
* fix(csharp): adaptive tree-sitter buffer + frozen-bucket clone for cross-namespace siblings (#1066)
Two coupled regressions surfaced when analyzing real-world C# repos with
large source files (issue #1066):
1. Tree-sitter `parser.parse()` is hard-coded to a 32 KB buffer by
default. Any file exceeding that threshold throws `Invalid argument`
on the worker re-parse path of `populateCsharpNamespaceSiblings`
(and the analogous Python / TypeScript captures fallbacks).
2. After the buffer fix unblocks the AST walk, the hook tries to
`push()` onto the inner `BindingRef[]` array fetched from
`indexes.bindings` — but `materializeBindings` froze that array via
`Object.freeze(refs.slice())`. Result: `Cannot add property N,
object is not extensible`.
Fixes:
- `csharp/captures.ts`, `python/captures.ts`, `typescript/captures.ts`:
pass `bufferSize: getTreeSitterBufferSize(sourceText.length)` to
`parser.parse()` on the cache-miss path so multi-MB files parse.
- `csharp/namespace-siblings.ts`: introduce `cloneBindingBucket` to
copy the frozen array before mutating, then `set()` the new array
back. This is a working but architecturally compromised workaround
(#1050 follow-up will replace it with an explicit augmentation
channel — see docs/plans/2026-04-26-001 plan).
Tests:
- New `csharp-large-cache-miss-resolution` fixture (Models/Services/
Other layout, ~77 KB padded UserService.cs) drives the buffer-size
failure end-to-end through worker mode.
- `csharp.test.ts`: 4 new regression assertions covering both the
parse-time buffer-size failure and the freeze workaround.
- Per-language captures unit tests gain "large cache-miss file uses
adaptive buffer" coverage (TS, Python, C#).
- `csharp-hooks.test.ts`: in-memory freeze regression test that
reproduces the `Cannot add property` crash without invoking the C#
parser at all.
Made-with: Cursor
* refactor(scope-resolution): add bindingAugmentations channel to indexes
Step 1 of the binding-augmentation-channel refactor (issue #1066
follow-up). Pure shape change — no consumers yet.
Adds a new `readonly bindingAugmentations` field to
`ScopeResolutionIndexes` initialized as an empty `Map` by
`finalizeScopeModel`. The new channel is the dedicated post-finalize
write target for hooks like `populateCsharpNamespaceSiblings`, so
`indexes.bindings` can stay frozen and finalize-owned.
Behavior unchanged: nothing reads or writes the new field yet. tsc and
the full unit suite remain green.
Plan: docs/plans/2026-04-26-001-binding-augmentation-channel.md (local
only — `docs/plans/` is gitignored).
Made-with: Cursor
* feat(scope-resolution): add lookupBindingsAt dual-source helper
Step 2 of the binding-augmentation-channel refactor. Introduces a
single primitive every walker uses to read both the finalize-owned
`indexes.bindings` channel and the post-finalize
`indexes.bindingAugmentations` channel.
Contract:
- Finalized refs come first (preserves existing precedence).
- Augmented refs append, deduped by `def.nodeId`.
- Empty input on both channels returns a shared frozen empty array.
- Single-channel hits return the bucket by reference (no allocation).
No consumers are wired yet — Step 3 routes the existing walker
primitives through this helper. Augmentations remain empty for every
language; behavior of the full suite is unchanged.
8 unit tests pin precedence, dedup, identity for single-channel hits,
and the shared-empty-frozen-array sentinel.
Made-with: Cursor
* refactor(scope-resolution): route binding lookups through lookupBindingsAt
Step 3 of the binding-augmentation-channel refactor. Every direct
`indexes.bindings.get(...)` consumer in the post-finalize phase is
now routed through `lookupBindingsAt` (per-name) or `namesAtScope`
+ `lookupBindingsAt` (bulk iteration).
Routed sites:
- `findClassBindingInScope` (walkers.ts) — class-receiver lookups.
- `findCallableBindingInScope` (walkers.ts) — free-call lookups.
- `findExportedDefByName` (walkers.ts) — module-scope-fallback
callable lookups.
- `propagateImportedReturnTypes` (passes/imported-return-types.ts)
— bulk iteration over an importer's binding entries; switched to
`namesAtScope` + per-name `lookupBindingsAt` so post-finalize
augmentations are visible to import-derived typeBinding mirrors.
Behavior unchanged: augmentations are empty across the suite (Step 4
populates them for C# `populateNamespaceSiblings`). 587
scope-resolution unit tests + 50 integration resolver suites green
(4 pre-existing Swift method-implements failures unrelated to this
work).
Adds `namesAtScope` companion helper for the bulk-iteration callers.
Made-with: Cursor
* refactor(csharp): write namespace siblings to bindingAugmentations channel
Step 4 of the binding-augmentation-channel refactor. The C#
`populateNamespaceSiblings` hook is the only consumer that needed
to inject cross-file bindings post-finalize, and prior to this
change it cloned the (frozen) finalized `BindingRef[]` arrays
through a `cloneBindingBucket` helper, then `set()`-back the new
array — a workaround for the `Object.freeze` applied by
`finalize-algorithm.ts` (issue #1066 root cause).
Architecturally that violated `ScopeResolver` Invariant I8 (which
permits post-finalize modifications but not in-place mutation of
finalized buckets). It also forced read-side consumers to be aware
of the workaround.
This change:
* Switches the three C# write sites to append into
`indexes.bindingAugmentations` via `getAugmentationBucket`. The
augmentation channel was added in Step 1 and is mutable by
contract: inner `BindingRef[]` arrays here are NEVER frozen.
* Deletes `cloneBindingBucket` and `getMutableScopeBindings`
(workaround helpers no longer needed).
* `lookupBindingsAt` (Step 2) merges the two channels transparently
for every walker (Step 3), so behavior is unchanged for callers.
* Updates the unit test to assert against both channels: finalized
bucket stays frozen and untouched, cross-file siblings show up in
augmentations only. Renamed the test accordingly.
Validation:
* `npx tsc --noEmit` clean.
* csharp hooks unit + walkers-augmentations unit + csharp integration
resolver suite all green (236/236).
* Wider `test/unit/scope-resolution test/integration/resolvers`
suite: 2507 pass, only 4 pre-existing Swift METHOD_IMPLEMENTS
failures remain (unrelated to this work, present on baseline).
Refs: issue #1066, ADR-pending binding-augmentation-channel.
Made-with: Cursor
* feat(scope-resolution): tighten I8 + add validateBindingsImmutability dev guard
Step 5 of the binding-augmentation-channel refactor. Captures the
new two-channel binding lifecycle in the contract docs and adds a
dev-mode runtime validator so a future hook cannot silently drift
back into mutating `indexes.bindings`.
Contract changes:
* `contract/scope-resolver.ts` — rewrote Invariant I8 to describe
the two channels (`indexes.bindings` is finalize-output and
immutable post-finalize; `indexes.bindingAugmentations` is the
append-only post-finalize channel populated by hooks like
`populateNamespaceSiblings`). Documented `lookupBindingsAt` as
the read-side merger and pointed at the new validator as the
enforcement mechanism.
* `gitnexus-shared/src/scope-resolution/types.ts` — extended the
module-header lifecycle contract to call out
`bindingAugmentations` alongside `ReferenceIndex` as the two
structures populated after the freeze.
Validator:
* New `pipeline/validate-bindings-immutability.ts` mirrors the
shape of `validateOwnershipParity` (#909): runs only when
`NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`,
emits via `onWarn`, never throws. Asserts (a) every inner
`BindingRef[]` in `indexes.bindings` is `Object.isFrozen`, and
(b) every inner array in `indexes.bindingAugmentations` is NOT
frozen.
* Wired into `pipeline/run.ts` after both
`populateNamespaceSiblings` and `propagateImportedReturnTypes`,
before `resolveReferenceSites`. One sweep covers the full
post-finalize surface.
Tests:
* `validate-bindings-immutability.test.ts` — 6 cases pinning happy
path, both drift directions, multi-violation accumulation, and
both production no-op gates.
All scope-resolution + csharp resolver tests green (242/242 in the
focused run; matches the wider Step 4 baseline).
Made-with: Cursor
* fix(ingestion): size tree-sitter buffers from UTF-8 bytes
Tree-sitter buffer sizing is byte-based, so computing adaptive buffers from JavaScript string length under-sized UTF-8-heavy files. Make getTreeSitterBufferSize accept source text directly and compute Buffer.byteLength internally, then update all parse call sites and max-buffer skip checks to use byte length.
Add multibyte cache-miss and cap regressions for C#, Python, TypeScript, and the C# namespace-sibling fallback parse path.
Made-with: Cursor
* test(scope-resolution): pin augmentation read paths
Add focused unit coverage for augmented-only binding reads across the routed walker helpers and imported-return-type propagation path. Clarify I8 wording around lexical Scope.bindings versus post-finalize index channels, and document the intentional local-only behavior of findExportedDef.
Also switch the immutability validator tests to Vitest env stubs, document one intentional validator blind spot, and split C# namespace-sibling tests so UTF-8 parsing and augmentation-channel behavior are asserted independently.
Made-with: Cursor
* test(scope-resolution): avoid slow parser stress fixtures
Replace high-cardinality large-file capture fixtures with large padding plus a trailing declaration. This still proves adaptive tree-sitter buffers parse beyond large ASCII and UTF-8-heavy input, without making query matching process thousands of declarations and risking timeouts.
Made-with: Cursor
* test(scope-resolution): add python and typescript cache-miss resolver regressions
Add worker-mode resolver integration coverage mirroring the C# #1066 scenario for Python and TypeScript. Each test builds a temp fixture with large ASCII and UTF-8-heavy source padding, then asserts trailing declarations and call edges still resolve after scope-resolution cache-miss reparsing.
Made-with: Cursor
* refactor(scope-resolution): gate I8 validator and fast-path namesAtScope
Addresses SPARC reviewer feedback on the binding-augmentation channel:
- Validator gate is now opt-in outside development. Extract
isSemanticModelValidatorEnabled() in utils/env.ts as the single
predicate; both validateBindingsImmutability and phase.ts's warn
handler share it. Default CLI runs no longer pay the O(binding-buckets)
scan, and explicit VALIDATE_SEMANTIC_MODEL=1 now emits warnings even
when NODE_ENV is unset.
- namesAtScope returns Iterable<string> and zero-allocates when at most
one channel is populated (returns Map.keys() directly), only
materializing a Set when both channels carry names. The caller-side
branching and EMPTY_NAMES escape hatch in propagateImportedReturnTypes
are gone -- both helpers handle the empty-augmentation case internally.
- C# namespace-siblings header/JSDoc, model JSDoc, I8 contract prose, and
the #1066 integration-test header rewritten to say post-finalize fanout
appends only to bindingAugmentations; finalized refs come first and win
duplicate def.nodeId metadata; local lexical Scope.bindings remains the
first-tier shadowing channel.
Validator unit-test setup deduplicated via beforeEach and extended with
default-CLI no-op + explicit-opt-in cases.
Made-with: Cursor
* feat(ingestion): TypeScript registry-primary scope resolution (Ring 3)
- Add TypeScript ScopeResolver stack (query/captures/interpret, import decomposition, hooks, arity, merge, receiver binding) and register in SCOPE_RESOLVERS.
- Harden shared compound receiver and receiver-bound CALLS pass for map for-of tuple bindings, dotted typeRef shapes, and callable-alias fallbacks.
- Flip TypeScript into MIGRATED_LANGUAGES; refresh AGENTS.md and type-resolution-system.md.
- Shared finalize-algorithm updates for cross-file scope parity.
- Tests: TS scope-resolution unit suite; legacy call-processor suite forces REGISTRY_PRIMARY_TYPESCRIPT=0; registry-primary flag test opts out TS in override scenario.
Made-with: Cursor
* fix(ingestion): SCC-ordered cross-file return-type propagation + multi-hop re-export resolution
Fix CI failures on PR #1050 (TypeScript registry-primary migration) by
making `propagateImportedReturnTypes` deterministic via reverse-
topological SCC ordering and updating the multi-hop re-export contract
to match `followReexportChain` behavior.
Why: the legacy pass mirrored an intermediate ref instead of the
terminal type when an importer was processed before its source module
had its own typeBindings chain-followed (4-file alias chain regression
in `ts-simple` fixture: `models.User -> service.user -> app.user`
collapsed to `getUser` instead of `User`). Reverse-topological walk of
`indexes.sccs` (leaves first) lets every importer see the source's
already-followed terminal type in a single pass.
Changes:
- `imported-return-types.ts`: rewrite to walk SCCs leaves-first, chain-
follow the source module's typeBindings BEFORE mirroring, and chain-
follow the importer's typeBindings AFTER mirroring. Cyclic SCCs
reach a partial fixpoint (no convergence guarantee, ts-circular only
asserts no-throw).
- `finalize-algorithm.ts`: docstring update on `FinalizeFile.localDefs`
to reflect that `followReexportChain` resolves multi-hop re-exports
through barrels even when intermediates do not surface the name -
surfacing is now a static optimization, not a correctness requirement.
- `contract/scope-resolver.ts` Invariant I3: explicitly document the
SCC ordering requirement.
- `pipeline/run.ts`: split PROF timer into `finalize` and `propagate`
so the pass's cost is observable independently.
- `ARCHITECTURE.md` Performance notes: describe SCC-ordered propagation.
- `imported-return-types.ts`: expand chain-depth comment (2x effective
depth from pre/post follow), add multi-ref break rationale, add
`ts-simple` motivating-fixture pointer.
Tests:
- `finalize-algorithm.test.ts`: add 4 cases (3-hop chain, cyclic
re-export visited-set guard, wildcard re-export fall-through,
multi-source first-match-wins); fix misleading shared nodeId in the
thick variant; rename and update the multi-hop test for the new
contract (transitiveVia assertion on the thin variant).
- `imported-return-types.test.ts` (NEW): unit tests for the SCC pass
pinning topological collapse, local-annotation guard, missing-source
skip, and cyclic-SCC no-throw.
- `cross-file-binding.test.ts` + `ts-deep-alias-chain` fixture (NEW):
5-file integration regression guard for SCC-ordered propagation
through 4 module boundaries.
Validation: 865 scope-resolution + cross-file tests pass on Windows;
typecheck clean across both packages; only pre-existing Swift overload
failures remain (verified on PR base commit, environmental).
Made-with: Cursor
* fix(ingestion): address PR #1050 review findings — side-effect imports, resolve-cache perf, adapter signature
Three independent fixes surfaced by the production-readiness review of
the TypeScript registry-primary scope-resolution migration (RFC #909
Ring 3). All three pass under both REGISTRY_PRIMARY_TYPESCRIPT=0 and =1.
1. Side-effect imports were silently dropped (correctness regression).
The legacy DAG emitted IMPORTS edges for `import './polyfill'` because
its tree-sitter query matches `(import_statement source: (string))`
regardless of clause. The new registry-primary path returned `[]`
from `splitImportStatement()` for clause-less imports, so no
ParsedImport / ImportEdge was ever produced — silent file-level edge
loss. Add a generic 'side-effect' variant to `ParsedImport` and
`ImportEdge['kind']` in `gitnexus-shared`; finalize resolves the
target file and pre-finalizes the edge (no `targetDefId`, no
`BindingRef`) so the SCC fixpoint loop skips it. The TypeScript
provider now emits + interprets the new kind end-to-end. The
variant is intentionally generic so other languages (Rust
`use foo as _`, Python module-init) can adopt it.
2. Per-import re-derivation in `resolveImportTarget` (perf regression).
The TS adapter built `new Set(allFilePaths)` on every call and let
`resolveTsImportTarget` re-derive `allFileList` /
`normalizedFileList` and discard the `resolveCache`. For a workspace
with N files and M imports that's O(N × M) work per pass. Wrap the
adapter in a closure that memoizes all five derived values keyed on
the orchestrator's `ReadonlySet` identity; reset only when the set
reference changes (start of new pass). New cost: O(N + M).
3. Misleading fake `ParsedImport` in the adapter (architecture).
The adapter constructed `{ kind: 'named', localName: '_',
importedName: '_', targetRaw }` to call `resolveTsImportTarget`,
even though only `targetRaw` and the structural-typed context are
read. Extract `resolveTsTarget(targetRaw, ctx)` so the adapter has
an honest signature; `resolveTsImportTarget` still works for other
callers. Also extract `narrowTsContext` for the type narrowing.
Tests: - New 4-file fixture `typescript-side-effect-imports` with two
side-effect imports + one named import.
- New "TypeScript side-effect imports" describe in
`test/integration/resolvers/typescript.test.ts` (parity-gated by
`ci-scope-parity.yml` — runs under both flag states).
- Updated 2 unit tests to expect 1 side-effect ParsedImport and 4
`@import.statement` matches (was 0 / 3).
- 785 / 785 TS scope-resolution tests pass under both
REGISTRY_PRIMARY_TYPESCRIPT=0 and =1.
Made-with: Cursor
* fix(scope): address Codex adversarial review findings on PR #1050
Four findings from the Codex adversarial review broke registry-primary
TypeScript resolution for common patterns. All four now have unit and
integration regression coverage that pass under both
`REGISTRY_PRIMARY_TYPESCRIPT=0` (legacy DAG) and the default
registry-primary path.
[high] tsconfig path aliases dropped:
Threaded `tsconfigPaths` through ScopeResolver via a new opaque
`resolutionConfig` parameter and a `loadResolutionConfig(repoPath)`
hook. The orchestrator (`scopeResolutionPhase` + `runScopeResolution`)
loads it once per workspace pass and forwards into every
`resolveImportTarget` call. TypeScript resolver now resolves
`@/services/user` style imports through the standard resolver's alias
branch.
[high] TSX parsed with the wrong grammar:
`emitTsScopeCaptures` now picks the parser/query by `filePath`
(`.tsx` -> TSX grammar) and validates cached trees against the
expected grammar via the new exported `tsCachedTreeMatchesGrammar`
helper. Stale TS-grammar trees for `.tsx` files no longer leak through
the scope query.
[medium] Literal dynamic imports never linked:
Added `kind: 'dynamic-resolved'` to `ParsedImport` and `ImportEdge`.
The decomposer emits a synthetic `@import.literal` capture for
string-literal dynamic imports; the interpreter maps that to
`dynamic-resolved`; finalize pre-finalizes it as a file-level terminal
(same shape as `side-effect`). `import('./feature')` now produces a
real IMPORTS edge under the registry-primary path. Legacy DAG keeps
its existing behavior — the new integration assertion is gated behind
the flag.
[medium] Namespace re-exports invisible from barrels:
The decomposer now emits TWO captures for `export * as ns from './m'`
— the existing `reexport-namespace` import draft AND a synthetic
`@declaration.namespace` capture (via `buildNamespaceDeclarationMatch`).
The latter creates a Namespace `SymbolDefinition` in the barrel's
`localDefs`, so downstream `import { ns } from './barrel'` resolves
through `findExportByName`.
Regression fixtures under `gitnexus/test/fixtures/lang-resolution/`:
- typescript-tsconfig-aliases (`@/` alias)
- typescript-tsx-jsx (Button.tsx + App.tsx with JSX)
- typescript-dynamic-import (`await import('./feature')`)
- typescript-reexport-namespace (`export * as Models from './base'`)
Validation:
- gitnexus-shared builds clean
- gitnexus typecheck clean
- 385/385 TS scope-resolution tests pass under both
`REGISTRY_PRIMARY_TYPESCRIPT=0` and default
Made-with: Cursor
* perf(scope): O(1) defById lookup + bounded re-export depth (PR #1050 round 3)
Addresses the round-3 PR #1050 reviews (Claude adversarial + xkonjin):
both flagged the existing O(N²) `findDefById` linear scan in
`materializeBindings` and the unbounded recursion in
`followReexportChain` as production-readiness blockers for TypeScript
monorepos. Both fixes land alongside their regression tests under
both `REGISTRY_PRIMARY_TYPESCRIPT=0` and the default registry-primary
path.
[high] materializeBindings O(N_files × N_defs × N_edges) → O(N_defs + N_edges):
Build a `nodeId → SymbolDefinition` index map once at the top of
`materializeBindings` (one O(N_defs) pass), then replace the per-edge
`findDefById(files, edge.targetDefId)` linear scan with an O(1)
`defById.get(edge.targetDefId)` lookup. Also drop the now-unused
`findDefById` helper. At realistic TypeScript monorepo scale (~5k
files × ~50 defs/file × ~100k linked import edges) this is the
difference between ~25 s and a few ms inside finalize. Regression
test in `finalize-algorithm.test.ts` builds 200 leaf files +
1 consumer importing one symbol from each, asserts every binding
materializes correctly.
[medium] followReexportChain unbounded recursion:
The existing `visited` set caps depth at `O(N_files)` but allows
recursion proportional to barrel-chain depth, mismatching the
explicit "Iterative DFS to avoid stack overflow" policy in
`tarjanSccs`. Added a `MAX_REEXPORT_DEPTH = 100` constant and a
`depth` parameter to `followReexportChain` (defaults to 0); each
recursive call passes `depth + 1` and the function returns `null`
when the cap is exceeded. 100 is comfortably above any realistic
hand-authored barrel chain (typical depth 1-5; auto-generated
barrels rarely exceed 20) while staying well below JS engine call
stack limits. Regression test wires a 200-link reexport chain and
verifies the crawl terminates cleanly with `linkStatus: 'unresolved'`
(no terminal def reachable within the budget).
[low] synthesizeInstanceofNarrowings bare-identifier-only limitation:
xkonjin's review #4 noted that the LHS narrowing only handles bare
identifiers (`if (x instanceof Foo)`), not member expressions
(`if (user.address instanceof Address)`). Added a JSDoc note
explaining the constraint and pointing readers at field-type
resolution as the workaround for member-chain receivers.
Validation:
- gitnexus-shared builds clean
- gitnexus typecheck clean
- 413/413 tests pass under both flag states for finalize-algorithm +
TS unit + TS integration suites
- 972/972 tests pass across full scope-resolution + Python +
C# integration smoke (no cross-language regression)
Made-with: Cursor
* refactor(finalize): replace recursive followReexportChain with SCC-condensed iterative closure
The legacy `followReexportChain` walked re-export drafts via mutual
recursion guarded by a per-call visited set + a `MAX_REEXPORT_DEPTH`
ceiling. Recursion is fragile (call-stack ceiling, no bound on depth
that's actually meaningful), so this replaces it with a structurally
better algorithm: a precomputed per-file re-export closure built by
running Tarjan SCC over the re-export sub-graph and propagating names
in reverse-topological order with a bounded intra-SCC fixpoint.
Algorithm (`buildReexportClosures` in finalize-algorithm.ts):
1. Sub-graph: build the directed graph of `reexport` + `wildcard`
drafts only (regular/namespace/dynamic imports do not contribute).
2. SCC condensation: run the same iterative `tarjanSccs` already
used for the file-level import graph; output is in reverse-topo
order so out-of-SCC neighbors are always already-finalized.
3. Per-SCC propagation:
- Acyclic singleton: one pass populates from neighbors' closures.
- Cyclic SCC: bounded fixpoint capped at |SCC|+1 iterations.
With first-wins precedence the closure map is monotone, so
each name needs at most |SCC| hops to traverse the cycle.
Precedence (preserved from the recursive crawl):
- Named re-exports take precedence over wildcards.
- Within each kind, declaration order wins.
Lookup at finalize time becomes O(1) (`lookupReexportedName`), down
from O(chain_depth × drafts) per consult and recursive at that.
Properties vs the legacy implementation:
- Stack-safe by construction; no `MAX_REEXPORT_DEPTH` guard needed.
- 1000-hop barrel chains now resolve in full (legacy capped at 100
and surfaced anything deeper as `unresolved`).
- Cycles handled structurally via SCC, not via per-call visited set.
- Same observable semantics: every existing test passes unchanged.
Tests:
- Replace the obsolete `MAX_REEXPORT_DEPTH (200-hop chain stops
cleanly without stack overflow)` test (which asserted the OLD
bug — that deep chains failed to resolve) with a positive
1000-hop test that asserts full resolution + accurate
`transitiveVia`. Proves both the recursion is gone AND the
closure correctly inherits the leaf def across all hops.
- Update commentary on adjacent re-export tests to reference the
closure mechanism.
- Update `FinalizeFile.localDefs` JSDoc + import-decomposer.ts
inline doc to point at `buildReexportClosures` instead of the
removed function name.
Validation: - gitnexus-shared builds cleanly.
- gitnexus typechecks cleanly.
- 28/28 finalize-algorithm.test.ts tests pass (incl. new 1000-hop).
- 801/801 TypeScript scope-resolution tests pass under default
(registry-primary) AND `REGISTRY_PRIMARY_TYPESCRIPT=0` (legacy DAG).
- 404/404 Python + C# integration tests pass — no regression in
cross-language consumers of the shared `finalize`.
Made-with: Cursor
* fix(scope): remove non-null assertions from scope resolution
Made-with: Cursor
* fix(scope): address TypeScript review follow-ups
Made-with: Cursor
* fix(scope): address TypeScript import review follow-ups
Add regression coverage for non-binding import edges and circular TypeScript bindings so PR #1050 review concerns stay visible without changing runtime semantics.
Made-with: Cursor
On Windows, HOME env is often unset, causing cache to be written to
'./undefined/'. Using os.homedir() ensures cross-platform compatibility
while preserving HF_HOME priority.
Fixes#1068
The early Validate step ran on both workflow_call and push events, but
push events never populate inputs.tag (the tag comes from github.ref).
This regressed every real tag-push release — v1.6.3's Docker Build &
Push failed at that gate. The downstream Verify step already falls back
to GITHUB_REF, so the upfront guard only needs to cover workflow_call.
* chore(deps)(deps): bump lucide-react in /gitnexus-web
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.562.0 to 1.11.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.11.0/packages/lucide-react)
---
updated-dependencies:
- dependency-name: lucide-react
dependency-version: 1.8.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps)(deps): provide local Github SVG for lucide-react v1
lucide-react 1.0 removed all brand icons (Github, Gitlab, Facebook,
Slack, etc) per https://lucide.dev/guide/react/migration. Our
centralized icon module re-exported `Github` from lucide-react,
which now fails typecheck.
Replace the re-export with a local forwardRef component that mirrors
the lucide v0 GitHub mark and the LucideProps API. All consumers keep
importing `Github` from `@/lib/lucide-icons` unchanged.
Made-with: Cursor
* refactor(web): use Primer Octicons mark for local Github icon
Swap the local lucide v0 outline mark for a verbatim copy of Primer
Octicons `mark-github-{16,24}` — the icon set GitHub itself ships on
github.com (MIT, Copyright (c) GitHub Inc.).
Why this source over the alternatives is documented at the top of
`gitnexus-web/src/lib/lucide-icons.tsx`, including:
* the lucide v1 brand-icon removal context and migration link,
* the trademark vs. license distinction (MIT covers our right to
copy the SVG; trademark rules govern *use*, and we only use the
mark in permitted ways per GitHub's brand toolkit),
* why we didn't add `@primer/octicons-react`, `react-icons`, or
`simple-icons` (zero-dep policy for one icon),
* source URLs for both SVG variants.
The component still implements `LucideProps` and is drop-in compatible
with the existing import sites in Header, RepoAnalyzer and
AnalyzeOnboarding. The mark is now filled (matching github.com) rather
than stroke-outlined; lucide-only stroke props are accepted for type
parity but ignored. Both 16 and 24 variants are shipped so the mark
stays crisp at small sizes when consumers pass an explicit `size`.
Made-with: Cursor
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Final step of the iterative vite 5 -> 8 migration. This is the
substantive hop: Rolldown replaces Rollup, Oxc replaces esbuild,
Lightning CSS replaces esbuild for CSS, and vitest jumps to v4 (vitest
3 only peers with vite ^5||^6||^7).
Dep changes (gitnexus-web/package.json):
- vite ^7.3.2 -> ^8.0.10
- vitest ^3.2.4 -> ^4.1.5
- @vitest/coverage-v8 ^3.2.4 -> ^4.1.5
- @tailwindcss/vite ^4.1.18 -> ^4.2.4 (vite ^8 peer support starts at 4.2.2)
- tailwindcss ^4.2.2 -> ^4.2.4 (match the vite plugin minor)
- @vitejs/plugin-react already at 5.2.0 from iter 2 (vite ^8 peer included)
Test fix (heartbeat.test.ts):
- vitest 4 enforces [[Construct]] on mock implementations used with `new`.
The arrow function passed to .mockImplementation() in the EventSource
stub is now rejected with "() => { ... } is not a constructor". Switched
to a regular function declaration, which restores constructor semantics
without changing test behaviour. All 7 heartbeat tests pass again.
Coverage threshold tune (vitest.config.ts):
- vitest 4 ships AST-aware coverage remapping by default, which measures
reachable code more accurately than the legacy istanbul-style mapping.
Same 220 tests now report 9.44%/4.47%/7.24%/9.58% instead of just over
10% on each axis. Lowered thresholds to 9/4/7/9 to keep them as soft
regression floors rather than coverage targets. No tests removed.
What we deliberately did NOT change:
- vite.config.ts: the five resolve.alias entries (mermaid, anthropic deep
import, gitnexus-shared, @, @shared) all keep working under Rolldown.
server.fs.allow: ['..'] is unchanged in v8. The mermaid alias is
arguably MORE important now because vite 8.0.10 explicitly removed
format-sniffing module resolution from the JS resolver.
- engines.node: vite 8 has the same Node floor as vite 7
(^20.19.0 || >=22.12.0), already set in iter 2.
- CI setup-node pin: already at 20.19.0 from iter 2.
Verified locally (Node v22.14.0):
- npm install: clean (+11 / -55 / 27 changed; size shrinks because vite 8
bundles deps internally), no ERESOLVE on @tailwindcss/vite
- npx tsc -b --noEmit: clean
- npm test: 220/220 pass, 1.80s (~21x faster than vite 7's 3.05s)
- npm run test:coverage: passes new thresholds
- npm run build: clean, **539ms** with Rolldown (vs 11.41s on vite 7,
~21x speedup), bundle ~1% smaller than vite 7
Closes the iterative vite 5 -> 8 series (#1061 vite 6, #1062 vite 7,
this PR vite 8). Supersedes Dependabot #1040.
Made-with: Cursor
Step 2 of the iterative vite 5 -> 8 migration. Tightens engines.node
to satisfy vite 7's require(esm) floor; no vite.config.ts edits.
Changes:
- vite ^6.4.2 -> ^7.3.2
- @vitejs/plugin-react ^5.1.0 -> ^5.1.4 (npm picked 5.2.0 within ^5.1.4,
which already lists vite ^8 as a peer -> iter 3 won't need to re-bump)
- gitnexus-web engines.node: >=20.0.0 -> ^20.19.0 || >=22.12.0 (vite 7
requirement; gitnexus CLI engines untouched since CLI doesn't use vite)
- .github/actions/setup-gitnexus-web: pin node-version to '20.19.0' so we
don't depend on the floating "20" alias resolving to a high enough patch.
CLI-side actions stay on '20'.
Why no other config changes: vite 7's removed surfaces (sass legacy API,
splitVendorChunkPlugin, transformIndexHtml.transform, optimizeDeps.entries
glob semantics, CORS middleware order) are not used here. The five
resolve.alias entries (@, @shared, gitnexus-shared, anthropic deep import,
mermaid ESM) keep working - alias plugin precedence is unchanged.
Verified locally (Node v22.14.0, well above the new floor):
- npm install: clean, no peer warnings
- npx tsc -b --noEmit: clean
- npm test: 220/220 pass
- npm run build: clean (11.41s, dist tree shape identical, hashes
shifted as expected because vite 7 changed default build.target from
'modules' to 'baseline-widely-available' - bundle is 1-4% smaller)
Iter 3 (vite 8) will follow once this bakes on main.
Made-with: Cursor
Step 1 of the iterative vite 5 -> 8 migration for gitnexus-web. This
PR does the lowest-risk hop: vite 5 -> 6 only. No config or engine
changes are required because:
- @tailwindcss/vite@4.1.18 already lists vite ^6 in its peer range
- @vitejs/plugin-react@5.1.x supports vite ^6
- vitest@3.2.4 supports vite ^6 (peer ^5 || ^6 || ^7)
- vite 6 still supports Node 18/20/22, so engines.node >=20.0.0 stays
- None of vite 6's breaking changes (sass legacy API, postcss-load-config v6,
json.stringify default, environment API, fs.allow auto-detect) touch
this app's vite.config.ts / vitest.config.ts surface
Verified locally:
- npm install: clean, no peer warnings
- npx tsc -b --noEmit: clean
- npm test: 220/220 pass
- npm run build: clean, dist tree shape matches main
Subsequent PRs will land vite 6 -> 7 (engines + setup-node pin) and
vite 7 -> 8 (plugin-react/tailwindcss-vite/vitest co-bumps). This
supersedes Dependabot #1040, which jumped 5 -> 8 in one shot and broke
on @tailwindcss/vite peer resolution.
Made-with: Cursor
Wraps docker/build-push-action with a local composite action that retries
once on failure (upstream keeps retry out of the action per
docker/build-push-action#1422). Adds ignore-error=true on cache-to so GHA
cache export flakes don't fail an otherwise successful push.
- Emit `::notice::` in the resolve step when attempt 2 recovers from a
first-attempt failure, so silent retries are grep-able in run logs and
trending registry/cache flakes stay visible.
- Bind `retry-wait-seconds` via `env:` in the backoff step to match the
env-binding convention used elsewhere in docker.yml (TAG_INPUT, DIGEST,
TAGS) — no direct expression interpolation inside shell bodies.
Preserves existing contract end-to-end: SHA pin, provenance=max, sbom=true,
dual-registry push, `steps.build.outputs.digest` wiring to Cosign and the
build-provenance attestations.