diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1a953fd71 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Code owners + +* @Arvuno +* @magyargergo diff --git a/.github/workflows/ci-scope-parity.yml b/.github/workflows/ci-scope-parity.yml deleted file mode 100644 index 039438a15..000000000 --- a/.github/workflows/ci-scope-parity.yml +++ /dev/null @@ -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/.test.ts` -# TWICE on every PR: -# -# 1. `REGISTRY_PRIMARY_=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_=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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32777fcfd..9ea361fb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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..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 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8ab8342d6..c7a95e29a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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 }}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9c4ba0d8f..4a73329ed 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index 7cfc3bc52..f5e9af170 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 122130310..a5265d073 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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] diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5c24cfa29..1de6cd5db 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -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 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 76f995c2a..a8ca6c839 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -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 }} diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 8b121b7c1..b74387116 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -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 diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 000000000..769b7eee9 --- /dev/null +++ b/.gitleaks.toml @@ -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''', +] diff --git a/AGENTS.md b/AGENTS.md index 286fbc14f..1e31004e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d39fef3a1..f65c175f9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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/` | @@ -147,105 +147,18 @@ export const myPhase: PipelinePhase = { --- -## 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_` 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 +168,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 +178,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 +199,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`, reads per-file Trees from the parse phase's `scopeTreeCache`, disposes the cache at the end. ### `ScopeResolver` contract @@ -312,7 +225,6 @@ Single interface a language implements to plug into the pipeline. Contract fully 1. Implement `ScopeResolver` in `languages//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 +240,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 | @@ -351,7 +262,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 +287,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 @@ -403,20 +314,21 @@ Unified 3-tier algorithm (`model/resolution-context.ts`), per-language `importSe 1. Worker pool dispatches files (or sequential fallback via `skipWorkers`) 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 +### Inheritance and MRO -All languages emit unified `ExtractedHeritage` (child, parent, `EXTENDS`/`IMPLEMENTS`). MRO phase walks the heritage graph using per-language strategy: +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 diff --git a/CLAUDE.md b/CLAUDE.md index e3815af76..bbb991589 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4cb50c8c3..ddefad384 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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` diff --git a/README.md b/README.md index 60f15861c..28229c98c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ OpenSSF Scorecard + + CI Workflows +

Enterprise (SaaS & Self-hosted) - akonlabs.com

@@ -226,6 +229,8 @@ 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 @@ -273,6 +278,36 @@ 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 ` 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. diff --git a/TESTING.md b/TESTING.md index e69a4b8e9..72833d3ca 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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_` flag were removed in RING4-1 (#942). Each language's resolver test lives at `test/integration/resolvers/.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_=0` — legacy DAG path -2. `REGISTRY_PRIMARY_=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/.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. diff --git a/eval/uv.lock b/eval/uv.lock index a78eadf06..2fe9e2d69 100644 --- a/eval/uv.lock +++ b/eval/uv.lock @@ -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]] diff --git a/gitnexus-shared/src/scope-resolution/symbol-definition.ts b/gitnexus-shared/src/scope-resolution/symbol-definition.ts index d1b30abcc..06814db3c 100644 --- a/gitnexus-shared/src/scope-resolution/symbol-definition.ts +++ b/gitnexus-shared/src/scope-resolution/symbol-definition.ts @@ -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; } diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index b9719e46b..a69275159 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -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": { diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index f6943cbf2..c09ad428f 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -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", diff --git a/gitnexus/bench/python-scope/baseline-fingerprint.txt b/gitnexus/bench/python-scope/baseline-fingerprint.txt index e8337a6d9..13a0b5ee3 100644 --- a/gitnexus/bench/python-scope/baseline-fingerprint.txt +++ b/gitnexus/bench/python-scope/baseline-fingerprint.txt @@ -1 +1 @@ -06687dff942d531c4d453b5906a8666c90db4867eb43ed18304aa59a8a93ef9d +c03f87cd8cd1ee716dea93cceb27109ee5b4584bc9fe426eea3f18fd6f9854cc diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 555b8e1f7..11fa8c92c 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -16,33 +16,34 @@ "_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." }, "cpp": { - "fingerprint": "538e8beebf0a69f6170dff452da3f98046a08cbe8b098b3c9943c4a8a79d2e22", + "fingerprint": "6d6207ae1df3943c5fae28983e0c294e55225456e7cf39af1d46fda21b6787c4", "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, 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." + "_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 \u2014 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 \u2014 pure fixture-corpus drift; fixture_count 270->272, fingerprint 538e8be->d63ded6. #1993: + cpp-cross-namespace-same-tail fixture \u2014 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", + "_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.", + "fingerprint": "7e8845040540ae69ef564ebf597305ade31e4480cee1dc910ea0fcfc26794910", "scaling_budget": 1.5 }, "rust": { - "fingerprint": "56ffc1c069af10cac3c82a32f3d148322ea570e116ebaae67315445f05407fef", + "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). #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.", - "_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)." + "_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) \u2014 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 \u2014 @declaration.macro/@reference.macro + MacroRegistry \u2192 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 \u2014 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": "bf6b13a366e4116da3772f9a9fdd50517eb11da73918451392e014a2c905b2dd", + "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." + "_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 \u2014 fixture count 78\u219281, fingerprint drift expected. #1975: + ruby-tail-collision fixture (Foo::Bar vs Baz::Bar stay distinct nodes) \u2014 pure fixture-corpus drift, scope-extractor captures unchanged; 81\u219282. #1991: + ruby-nested-mixin-tail-collision fixture (85\u219286). Recomputed on the #942 merge (fixture-comment rewording shifts capture byte-positions, capture LOGIC unchanged): bf6b13a -> b5ea93bb." }, "swift": { "fingerprint": "53325c6345161c5a495f997297af5a24fb718fd3e6647040160f8ab2a2c8e4c0", @@ -56,9 +57,9 @@ "_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." }, "java": { - "fingerprint": "b63f9be458f7ece854e7b007159d7bf65b4b66a86e83a6c0656fc93ebd5d83da", + "fingerprint": "d5cf68e9faf92fffd928c1ee6e584c72cc65918d1f1b5078abb3bfe09ac699bf", "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), 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), 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." }, "typescript": { "fingerprint": "3f44a4a6892698df2d145c8ff2812c3b318807648983c88aca28fbd694f172f9", @@ -67,15 +68,15 @@ "_note": "#1968: F44, F85, F87 \u2014 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": "a16400622892183581b8f5f8fa01f07842d19b8cf49ed021df52bd17009d749f", "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": "#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). | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged. | #1930 F45: default parameters now emit optional-arity metadata; capture fingerprint changes, scaling remains linear." } } diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index e6fbd246b..810923e40 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -5039,9 +5039,9 @@ "optional": true }, "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": { diff --git a/gitnexus/package.json b/gitnexus/package.json index 1e8a45812..53eb7dedd 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -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", diff --git a/gitnexus/scripts/bench-scope-resolution.ts b/gitnexus/scripts/bench-scope-resolution.ts index 399d44fcf..32020a2c0 100644 --- a/gitnexus/scripts/bench-scope-resolution.ts +++ b/gitnexus/scripts/bench-scope-resolution.ts @@ -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'; diff --git a/gitnexus/scripts/ci-list-migrated-languages.ts b/gitnexus/scripts/ci-list-migrated-languages.ts deleted file mode 100644 index 732ce861c..000000000 --- a/gitnexus/scripts/ci-list-migrated-languages.ts +++ /dev/null @@ -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/.test.ts`. - * - `envvar`: uppercase suffix used to build the `REGISTRY_PRIMARY_` 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)); diff --git a/gitnexus/scripts/run-parity.ts b/gitnexus/scripts/run-parity.ts deleted file mode 100644 index 6fbf35dc6..000000000 --- a/gitnexus/scripts/run-parity.ts +++ /dev/null @@ -1,139 +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_=0 (legacy DAG) - * 2. Run its resolver test with REGISTRY_PRIMARY_=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 testFilePaths(slug: string): string[] { - const resolverDir = path.resolve(ROOT, 'test/integration/resolvers'); - const files = fs.readdirSync(resolverDir); - const direct = `${slug}.test.ts`; - const prefixed = `${slug}-`; - return files - .filter((name) => name === direct || (name.startsWith(prefixed) && name.endsWith('.test.ts'))) - .sort() - .map((name) => `test/integration/resolvers/${name}`); -} - -function runVitest(testFile: string, env: Record): 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[] = []; -const filesByLanguage = new Map(); -for (const lang of languages) { - const files = testFilePaths(lang); - filesByLanguage.set(lang, files); - if (files.length === 0) { - missingFiles.push(`test/integration/resolvers/${lang}*.test.ts (${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 files = filesByLanguage.get(lang) ?? []; - const envVar = envVarName(lang); - - console.log(`\n── ${lang} — legacy DAG (${envVar}=0) ──`); - for (const file of files) { - if (!runVitest(file, { [envVar]: '0' })) { - failures.push({ lang, mode: 'legacy' }); - } - } - - console.log(`\n── ${lang} — registry-primary (${envVar}=1) ──`); - for (const file of files) { - if (!runVitest(file, { [envVar]: '1' })) { - failures.push({ lang, mode: 'registry-primary' }); - } - } -} - -// Summary -const total = [...filesByLanguage.values()].reduce((sum, files) => sum + files.length * 2, 0); -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.'); diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index 34de638b4..6470c6430 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -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 = ''; @@ -89,6 +95,16 @@ async function findGroupsContainingRegistryName(registryName: string): Promise 0 @@ -151,7 +175,7 @@ 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 \`gitnexus_detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: \`gitnexus_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"})\`. @@ -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 }; +} diff --git a/gitnexus/src/cli/analyze-config.ts b/gitnexus/src/cli/analyze-config.ts new file mode 100644 index 000000000..8fbe40feb --- /dev/null +++ b/gitnexus/src/cli/analyze-config.ts @@ -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 = { + 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, + { allowNestedKey }: { allowNestedKey: boolean }, +): Partial => { + const out: Partial = {}; + const setBy = new Map(); + + 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)[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 | 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; + const flat = normalizeLevel(obj, { allowNestedKey: true }); + + let nested: Partial = {}; + 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, { 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 | 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)[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; +} diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 227b929a1..6801f3f77 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -26,7 +26,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'; @@ -555,6 +562,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. */ @@ -639,16 +653,116 @@ export const analyzeCommand = async (inputPath?: string, options?: AnalyzeOption } }; -const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions): Promise => { - if (options?.verbose) { +const analyzeCommandImpl = async ( + inputPath?: string, + cliOptions?: AnalyzeOptions, +): Promise => { + 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'); @@ -660,7 +774,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'); @@ -677,7 +791,7 @@ 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) { cliError( @@ -695,7 +809,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( @@ -707,7 +821,7 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions): } embeddingsNodeLimit = parsed; } - const embeddingsEnabled = !!options?.embeddings; + const embeddingsEnabled = !!options.embeddings; const setPositiveEnv = ( optionName: string, @@ -729,23 +843,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'); @@ -755,7 +869,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', @@ -764,52 +878,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 @@ -939,36 +1019,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. @@ -988,6 +1071,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; @@ -997,6 +1095,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; @@ -1070,9 +1173,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, }, ); } diff --git a/gitnexus/src/cli/cli-message.ts b/gitnexus/src/cli/cli-message.ts index d21bb536e..da5f0b28e 100644 --- a/gitnexus/src/cli/cli-message.ts +++ b/gitnexus/src/cli/cli-message.ts @@ -52,7 +52,9 @@ export type RecoveryHint = | '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 diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index b599f7e9e..f64b3a24d 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -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 ', + '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', diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index a3ca9c898..4a9fba9af 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -1,303 +1,47 @@ +/** + * Route / fetch edge emission + exported-type-map helpers. + * + * The legacy call-resolution DAG that previously lived here (per-file type + * inference → receiver inference → dispatch selection → MRO walk over the + * legacy heritage map) was deleted in RING4-1 (#942): all languages now resolve + * calls through the scope-resolution registry pipeline. What remains are the + * language-agnostic edge emitters that are NOT part of call resolution: + * + * - `processRoutesFromExtracted` — CALLS edges from framework routes + * (e.g. Laravel) to their controller methods. + * - `processNextjsFetchRoutes` / `extractFetchCallsFromFiles` / + * `extractConsumerAccessedKeys` — FETCHES edges from `fetch()` calls to + * Next.js Route nodes. + * - `buildExportedTypeMapFromGraph` — exported symbol → return/declared type + * map, consumed by the cross-file enrichment pass. + */ + +import Parser from 'tree-sitter'; import { KnowledgeGraph } from '../graph/types.js'; import { ASTCache } from './ast-cache.js'; -import type { SymbolDefinition } from 'gitnexus-shared'; -import type { SymbolTableReader, HeritageMap, ExtractedHeritage } from './model/index.js'; -import { CLASS_TYPES, CALL_TARGET_TYPES, lookupMethodByOwnerWithMRO } from './model/index.js'; -import type { DispatchDecision, ReceiverEnriched } from './call-types.js'; - -/** Shorthand for the receiver-source discriminant shared across the DAG. */ -type ReceiverSource = ReceiverEnriched['receiverSource']; - -/** - * DAG stage 4 fallback: used when `selectDispatch` is absent or returns null. - * Preserves pre-DAG dispatch semantics: - * - 'constructor' → constructor branch - * - 'free' → free branch (admits class-target fast path) - * - 'member' or undefined → owner-scoped branch - * - * `undefined` callForm MUST route through owner-scoped (not free) so bare - * identifiers without a classified shape do NOT trigger `resolveFreeCall`'s - * class-target fast path. Without a `receiverTypeName`, the owner-scoped - * branch falls through to `resolveModuleAliasedCall` + `singleCandidate`, - * matching legacy behavior where non-callable symbols (Class, Interface) - * null-route instead of producing spurious Constructor edges. - */ -const defaultDispatchDecision = ( - callForm: 'free' | 'member' | 'constructor' | undefined, -): DispatchDecision => { - if (callForm === 'constructor') return { primary: 'constructor' }; - if (callForm === 'free') return { primary: 'free' }; - return { primary: 'owner-scoped' }; -}; -import Parser from 'tree-sitter'; -import type { ResolutionContext } from './model/resolution-context.js'; -import { TIER_CONFIDENCE, type ResolutionTier } from './model/resolution-context.js'; -import type { TieredCandidates } from './model/resolution-context.js'; +import type { SemanticModel, SymbolTableReader } from './model/index.js'; import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js'; import { getProvider } from './languages/index.js'; import { generateId } from '../../lib/utils.js'; -import { getLanguageFromFilename, SupportedLanguages } from 'gitnexus-shared'; -import { isRegistryPrimary } from './registry-primary-flag.js'; -import { isVerboseIngestionEnabled } from './utils/verbose.js'; -import { - ALWAYS_ON_SLOW_FILE_WARN_THROTTLE_MS, - alwaysOnSlowFileWarnMs, - deferredCallFileSlowMs, - deferredCallLogEveryN, - getDeferredProfileDroppedCount, - isDeferredResolutionProfileEnabled, - logDeferredProfile, - profileElapsedMs, - resetDeferredProfileDroppedCount, - startTimer, -} from './utils/deferred-resolution-profile.js'; +import { getLanguageFromFilename } from 'gitnexus-shared'; +import type { SymbolDefinition } from 'gitnexus-shared'; import { yieldToEventLoop } from './utils/event-loop.js'; import { parseSourceSafe } from '../tree-sitter/safe-parse.js'; -import { - CLASS_CONTAINER_TYPES, - FUNCTION_NODE_TYPES, - findEnclosingClassInfo, - genericFuncName, - inferFunctionLabel, -} from './utils/ast-helpers.js'; -import type { FieldInfo, FieldExtractorContext } from './field-types.js'; -import type { LanguageProvider } from './language-provider.js'; -import { typeTagForId, constTagForId, buildCollisionGroups } from './utils/method-props.js'; -import type { MethodInfo } from './method-types.js'; -import { - countCallArguments, - inferCallForm, - extractReceiverName, - extractReceiverNode, - extractMixedChain, - extractCallArgTypes, - type MixedChainStep, -} from './utils/call-analysis.js'; -import { buildTypeEnv, isSubclassOf } from './type-env.js'; -import type { ConstructorBinding, TypeEnvironment } from './type-env.js'; -import type { BindingAccumulator } from './binding-accumulator.js'; import { getTreeSitterBufferSize } from './constants.js'; -import type { - ExtractedCall, - ExtractedAssignment, - ExtractedRoute, - ExtractedFetchCall, - FileConstructorBindings, -} from './workers/parse-worker.js'; +import type { ExtractedRoute, ExtractedFetchCall } from './workers/parse-worker.js'; import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js'; -import { extractTemplateComponents } from './vue-sfc-extractor.js'; -import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js'; -import type { LiteralTypeInferrer } from './type-extractors/types.js'; -import type { SyntaxNode } from './utils/ast-helpers.js'; - -import { logger } from '../logger.js'; - -// ── Property-prepass helpers (parity with parse-worker.ts) ── -// These mirror the sequential-path equivalents in parse-worker.ts so the main- -// thread `processCalls` pre-pass produces byte-identical Property nodes/symbols -// to the worker pool. Drift between the two paths breaks the -// `incremental ≡ --force` invariant the moment a repo crosses the worker -// threshold between runs. - -/** Walk up to the nearest enclosing class/struct/interface AST node. */ -const findEnclosingClassNode = (node: SyntaxNode): SyntaxNode | null => { - let current = node.parent; - while (current) { - if (CLASS_CONTAINER_TYPES.has(current.type)) return current; - current = current.parent; - } - return null; -}; - -/** No-op SymbolTable stub for FieldExtractorContext — matches parse-worker. */ -const NOOP_SYMBOL_TABLE: SymbolTableReader = { - lookupExact: () => undefined, - lookupExactFull: () => undefined, - lookupExactAll: () => [], - lookupCallableByName: () => [], - getFiles: () => [][Symbol.iterator](), - getStats: () => ({ fileCount: 0 }), -}; - -/** - * Extract (and cache) field info for a class node. Cache is passed in so it - * stays scoped to a single `processCalls` invocation rather than leaking - * across analyze runs (worker uses module-level caching because each worker - * process is short-lived; the main thread is not). - * - * Cache key is `${filePath}:${classNode.startIndex}` — startIndex alone is a - * per-file byte offset, so almost every Ruby/Python file's leading class lands - * at byte 0 and would collide across files in the shared map. - */ -const getFieldInfo = ( - classNode: SyntaxNode, - provider: LanguageProvider, - context: FieldExtractorContext, - cache: Map>, -): Map | undefined => { - if (!provider.fieldExtractor) return undefined; - const cacheKey = `${context.filePath}:${classNode.startIndex}`; - const cached = cache.get(cacheKey); - if (cached) return cached; - const result = provider.fieldExtractor.extract(classNode, context); - if (!result?.fields?.length) return undefined; - const map = new Map(); - for (const field of result.fields) map.set(field.name, field); - cache.set(cacheKey, map); - return map; -}; - -/** Per-file resolved type bindings for exported symbols. - * Populated during call processing, consumed by Phase 14 re-resolution pass. */ -export type ExportedTypeMap = Map>; - -/** - * Type labels treated as class-like **method-dispatch receivers** by the call - * resolver — the set walked by the MRO / heritage path for member and static - * method calls. - * - * Derived from `CLASS_TYPES` (the heritage-index set in symbol-table) plus - * `Impl` — Rust `impl` blocks are the definition site of methods for a struct - * and must be walkable as receiver-type candidates even though they are not - * indexed by `lookupClassByName` (which keys off struct/trait names). Keeping - * this set a strict superset of `CLASS_TYPES` guarantees that anything - * reachable via `lookupClassByName` also passes this filter, so the two call - * paths cannot diverge silently. - * - * `Interface` is included even though interfaces cannot be directly - * instantiated in Java/C#/TypeScript: the resolver still needs to reach - * interface nodes for static-method dispatch (`Interface.staticMethod()`) and - * default-method resolution via the MRO walker. - * - * **Do not reuse this set for constructor-fallback filtering.** Constructors - * can only instantiate a narrower subset — see `INSTANTIABLE_CLASS_TYPES` - * below. `resolveStaticCall`'s step-5 class-node fallback uses the narrower - * set to prevent false `CALLS` edges from constructor-shaped calls to - * `Interface`, `Trait`, or `Impl` nodes. - */ -const CLASS_LIKE_TYPES = new Set([...CLASS_TYPES, 'Impl']); - -/** - * Type labels that can be the target of a constructor-shaped call when no - * explicit `Constructor` symbol is indexed — the "return the type itself as - * the call target" fallback set. - * - * Strict subset of both `CLASS_LIKE_TYPES` and `CONSTRUCTOR_TARGET_TYPES`. - * Excludes: - * - `Interface` / `Trait` — not instantiable by definition in any - * supported language. - * - `Impl` — Rust `impl` blocks are method-definition containers, not - * the type itself; the owning `Struct` is the correct target. - * - `Enum` — excluded pending language-specific support with motivating - * test fixtures (matches `CONSTRUCTOR_TARGET_TYPES`). - * - * Used exclusively by `resolveStaticCall`'s step-5 class-node fallback. - * Keep in sync with `CONSTRUCTOR_TARGET_TYPES` (which additionally contains - * `'Constructor'` for explicit-constructor-node filtering) when extending. - */ -const INSTANTIABLE_CLASS_TYPES = new Set(['Class', 'Struct', 'Record']); +import { extractReturnTypeName } from './type-extractors/shared.js'; const MAX_EXPORTS_PER_FILE = 500; const MAX_TYPE_NAME_LENGTH = 256; -/** Build a map of imported callee names → return types for cross-file call-result binding. - * Consulted ONLY when SymbolTable has no unambiguous local match (local-first principle). - * - * Overlapping mechanism (1 of 3): this is the SymbolTable-backed path. - * See also: - * 2. collectExportedBindings (~line 168) / enrichExportedTypeMap — TypeEnv + graph isExported - * 3. Phase 9 fallback in verifyConstructorBindings (~line 563) — namedImportMap + BindingAccumulator - * A future cleanup should merge these into a single resolution pass. */ -export function buildImportedReturnTypes( - filePath: string, - namedImportMap: ReadonlyMap< - string, - ReadonlyMap - >, - symbolTable: { - lookupExactFull(filePath: string, name: string): { returnType?: string } | undefined; - }, -): ReadonlyMap { - const result = new Map(); - const fileImports = namedImportMap.get(filePath); - if (!fileImports) return result; +/** Per-file resolved type bindings for exported symbols. + * Consumed by the cross-file re-resolution / enrichment pass. */ +export type ExportedTypeMap = Map>; - for (const [localName, binding] of fileImports) { - const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName); - if (!def?.returnType) continue; - const simpleReturn = extractReturnTypeName(def.returnType); - if (simpleReturn) result.set(localName, simpleReturn); - } - return result; -} - -/** Build cross-file RAW return types for imported callables. - * Unlike buildImportedReturnTypes (which stores extractReturnTypeName output), - * this stores the raw declared return type string (e.g., 'User[]', 'List'). - * Used by lookupRawReturnType for for-loop element extraction via extractElementTypeFromString. */ -export function buildImportedRawReturnTypes( - filePath: string, - namedImportMap: ReadonlyMap< - string, - ReadonlyMap - >, - symbolTable: { - lookupExactFull(filePath: string, name: string): { returnType?: string } | undefined; - }, -): ReadonlyMap { - const result = new Map(); - const fileImports = namedImportMap.get(filePath); - if (!fileImports) return result; - - for (const [localName, binding] of fileImports) { - const def = symbolTable.lookupExactFull(binding.sourcePath, binding.exportedName); - if (!def?.returnType) continue; - result.set(localName, def.returnType); - } - return result; -} - -/** Collect resolved type bindings for exported file-scope symbols. - * Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. - * - * **Counterpart**: the worker path populates `exportedTypeMap` via the - * accumulator enrichment loop in `pipeline.ts` (search for "Worker path - * quality enrichment"). Both sites populate the same map with subtly - * different export-check semantics — this site uses SymbolTable + - * graph lookup, the worker loop uses three-candidate-ID graph lookup. - * They must stay in sync until unified. If you edit one, check the other. - * - * Overlapping mechanism (2 of 3): this is the TypeEnv + graph isExported path. - * See also: - * 1. buildImportedReturnTypes (~line 109) — namedImportMap + SymbolTable - * 3. Phase 9 fallback in verifyConstructorBindings (~line 563) — namedImportMap + BindingAccumulator - * A future cleanup should merge these into a single resolution pass. */ -function collectExportedBindings( - typeEnv: { fileScope(): ReadonlyMap }, - filePath: string, - symbolTable: { lookupExact(filePath: string, name: string): string | undefined }, - graph: { getNode(id: string): { properties?: { isExported?: boolean } } | undefined }, -): Map | null { - const fileScope = typeEnv.fileScope(); - if (!fileScope || fileScope.size === 0) return null; - - const exported = new Map(); - for (const [varName, typeName] of fileScope) { - if (exported.size >= MAX_EXPORTS_PER_FILE) break; - if (!typeName || typeName.length > MAX_TYPE_NAME_LENGTH) continue; - const nodeId = symbolTable.lookupExact(filePath, varName); - if (!nodeId) continue; - const node = graph.getNode(nodeId); - if (node?.properties?.isExported) { - exported.set(varName, typeName); - } - } - return exported.size > 0 ? exported : null; -} - -/** Build ExportedTypeMap from graph nodes — used for worker path where TypeEnv - * is not available in the main thread. Collects returnType/declaredType from - * exported symbols that have callables with known return types. */ +/** Build ExportedTypeMap from graph nodes — used for the worker path where the + * sequential TypeEnv is not available in the main thread. Collects + * returnType/declaredType from exported symbols with known types. */ export function buildExportedTypeMapFromGraph( graph: KnowledgeGraph, symbolTable: SymbolTableReader, @@ -331,2962 +75,112 @@ export function buildExportedTypeMapFromGraph( return result; } -/** Seed cross-file receiver types into pre-extracted call records. - * Fills missing receiverTypeName for single-hop imported variables - * using ExportedTypeMap + namedImportMap — zero disk I/O, zero AST re-parsing. - * Mutates calls in-place. Runs BEFORE processCallsFromExtracted. */ -export function seedCrossFileReceiverTypes( - calls: ExtractedCall[], - namedImportMap: ReadonlyMap< - string, - ReadonlyMap - >, - exportedTypeMap: ReadonlyMap>, -): { enrichedCount: number } { - if (namedImportMap.size === 0 || exportedTypeMap.size === 0) { - return { enrichedCount: 0 }; - } - let enrichedCount = 0; - for (const call of calls) { - if (call.receiverTypeName || !call.receiverName) continue; - if (call.callForm !== 'member') continue; - - const fileImports = namedImportMap.get(call.filePath); - if (!fileImports) continue; - - const binding = fileImports.get(call.receiverName); - if (!binding) continue; - - const upstream = exportedTypeMap.get(binding.sourcePath); - if (!upstream) continue; - - const type = upstream.get(binding.exportedName); - if (type) { - call.receiverTypeName = type; - enrichedCount++; - } - } - return { enrichedCount }; -} - -// Stdlib methods that preserve the receiver's type identity. When TypeEnv already -// strips nullable wrappers (Option → User), these chain steps are no-ops -// for type resolution — the current type passes through unchanged. -const TYPE_PRESERVING_METHODS = new Set([ - 'unwrap', - 'expect', - 'unwrap_or', - 'unwrap_or_default', - 'unwrap_or_else', // Rust Option/Result - 'clone', - 'to_owned', - 'as_ref', - 'as_mut', - 'borrow', - 'borrow_mut', // Rust clone/borrow - 'get', // Kotlin/Java Optional.get() - 'orElseThrow', // Java Optional -]); - -/** Cache for method extraction results in findEnclosingFunction fallback path. - * Keyed by classNode.id to avoid re-extracting the same class body per call site. - * Cleared between files at line ~611 in the processCalls file loop. */ -const enclosingFnExtractCache = new Map< - number, - import('./method-types.js').ExtractedMethods | null ->(); +/** + * Confidence for route → controller-method CALLS edges. Framework-route + * controller references (e.g. `OrderController::class` in `routes/web.php`) + * resolve by global class name, so this matches the legacy `global`-tier + * confidence the tiered resolver previously assigned these edges. + */ +const ROUTE_EDGE_CONFIDENCE = 0.5; /** - * Walk up the AST from a node to find the enclosing function/method. - * Returns null if the call is at module/file level (top-level code). + * Resolve a route's controller class from its normalized dot-joined + * fully-qualified name (threaded by the Laravel extractor from a `use`/`::class` + * reference). Two strategies, in order: + * + * 1. Direct qualified lookup — works when the type registry keys the class by + * its FQN (block-form namespaces, non-PHP frameworks, seeded test models). + * 2. PSR-4 file-path disambiguation — PHP's common statement-form namespace + * (`namespace App\Http\Controllers;`) leaves the structure-phase + * `qualifiedName` as the *short* class name, so the registry has no FQN + * key. Instead, take the FQN's last segment as the class name, fetch the + * same-short-name candidates, and pick the one whose file path's tail + * matches the FQN's namespace tail (e.g. `App.Admin.OrderController` ↔ + * `app/Admin/OrderController.php`). Requires ≥2 trailing segments (class + + * ≥1 namespace segment) and a unique winner — conservative, so a + * non-PSR-4 layout falls through to short-name resolution rather than + * guessing. + * + * Returns the resolved class, or `undefined` when the FQN cannot be uniquely + * resolved (the caller then falls back to bare short-name resolution). */ -const findEnclosingFunction = ( - node: SyntaxNode, - filePath: string, - ctx: ResolutionContext, - provider: import('./language-provider.js').LanguageProvider, -): string | null => { - let current = node.parent; - - while (current) { - if (FUNCTION_NODE_TYPES.has(current.type)) { - const efnResult = provider.methodExtractor?.extractFunctionName?.(current, filePath); - const funcName = efnResult?.funcName ?? genericFuncName(current); - const label = efnResult?.label ?? inferFunctionLabel(current.type); - - if (funcName) { - const resolved = ctx.resolve(funcName, filePath); - if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) { - // Disambiguate by enclosing class when multiple candidates - if (resolved.candidates.length === 1) { - return resolved.candidates[0].nodeId; - } - const classInfo = findEnclosingClassInfo(current, filePath); - if (classInfo) { - const classMatches = resolved.candidates.filter((c) => c.ownerId === classInfo.classId); - // Unique class match — return it (no same-arity ambiguity) - if (classMatches.length === 1) return classMatches[0].nodeId; - // Multiple same-class candidates (same-arity overloads) — fall through - // to the fallback path which computes the exact ID with type-hash. - if (classMatches.length > 1) { - /* fall through to manual ID construction below */ - } else { - // No class match — return first candidate as before - return resolved.candidates[0].nodeId; - } - } else { - return resolved.candidates[0].nodeId; - } - } - - // Fallback: qualify the generated ID to match definition-phase node IDs - let finalLabel = label; - if (provider.labelOverride) { - const override = provider.labelOverride(current, label); - if (override !== null) finalLabel = override; - } - const classInfo2 = findEnclosingClassInfo(current, filePath); - const qualifiedName = classInfo2 ? `${classInfo2.className}.${funcName}` : funcName; - // Include # and ~typeTag suffix to match definition-phase Method/Constructor IDs. - const language = getLanguageFromFilename(filePath); - let arity: number | undefined; - let encTypeTag = ''; - if ( - (finalLabel === 'Method' || finalLabel === 'Constructor') && - provider.methodExtractor && - language - ) { - // Get class method map (cached per classNode.id) and look up current method - // by funcName:line. This avoids per-call-site extractFromNode AST walks. - let classNode = current.parent; - while (classNode && !provider.methodExtractor.isTypeDeclaration(classNode)) { - classNode = classNode.parent; - } - let info: MethodInfo | undefined; - if (classNode) { - let extracted = enclosingFnExtractCache.get(classNode.id); - if (extracted === undefined) { - extracted = - provider.methodExtractor.extract(classNode, { filePath, language }) ?? null; - enclosingFnExtractCache.set(classNode.id, extracted); - } - if (extracted?.methods?.length) { - const defLine = current.startPosition.row + 1; - info = extracted.methods.find((m) => m.name === funcName && m.line === defLine); - if (info) { - arity = info.parameters.some((p) => p.isVariadic) - ? undefined - : info.parameters.length; - } - if (arity !== undefined && info) { - const methodMap = new Map(); - for (const m of extracted.methods) methodMap.set(`${m.name}:${m.line}`, m); - const groups = buildCollisionGroups(methodMap); - encTypeTag = - typeTagForId(methodMap, funcName, arity, info, language, groups) + - constTagForId(methodMap, funcName, arity, info, groups); - } - } - } - // Fallback: extractFromNode for top-level methods without a class - if (!info && provider.methodExtractor.extractFromNode) { - const nodeInfo = provider.methodExtractor.extractFromNode(current, { - filePath, - language, - }); - if (nodeInfo) { - arity = nodeInfo.parameters.some((p) => p.isVariadic) - ? undefined - : nodeInfo.parameters.length; - } - } - } - const arityTag = arity !== undefined ? `#${arity}${encTypeTag}` : ''; - return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag}`); - } - } - - // Language-specific enclosing function resolution (e.g., Dart where - // function_body is a sibling of function_signature, not a child). - if (provider.enclosingFunctionFinder) { - const customResult = provider.enclosingFunctionFinder(current); - if (customResult) { - const resolved = ctx.resolve(customResult.funcName, filePath); - if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) { - if (resolved.candidates.length === 1) { - return resolved.candidates[0].nodeId; - } - const classInfo = findEnclosingClassInfo(current.previousSibling ?? current, filePath); - if (classInfo) { - const classMatches = resolved.candidates.filter((c) => c.ownerId === classInfo.classId); - if (classMatches.length === 1) return classMatches[0].nodeId; - if (classMatches.length > 1) { - /* fall through to manual ID construction below */ - } else { - return resolved.candidates[0].nodeId; - } - } else { - return resolved.candidates[0].nodeId; - } - } - let finalLabel = customResult.label; - if (provider.labelOverride) { - const override = provider.labelOverride(current.previousSibling!, finalLabel); - if (override !== null) finalLabel = override; - } - const classInfo2 = findEnclosingClassInfo(current.previousSibling ?? current, filePath); - const qualifiedName = classInfo2 - ? `${classInfo2.className}.${customResult.funcName}` - : customResult.funcName; - // Include # and ~typeTag suffix to match definition-phase Method/Constructor IDs. - const sigNode = current.previousSibling ?? current; - const language2 = getLanguageFromFilename(filePath); - let arity2: number | undefined; - let encTypeTag2 = ''; - if ( - (finalLabel === 'Method' || finalLabel === 'Constructor') && - provider.methodExtractor && - language2 - ) { - let classNode2 = (current.previousSibling ?? current).parent; - while (classNode2 && !provider.methodExtractor.isTypeDeclaration(classNode2)) { - classNode2 = classNode2.parent; - } - let info2: MethodInfo | undefined; - if (classNode2) { - let extracted2 = enclosingFnExtractCache.get(classNode2.id); - if (extracted2 === undefined) { - extracted2 = - provider.methodExtractor.extract(classNode2, { filePath, language: language2 }) ?? - null; - enclosingFnExtractCache.set(classNode2.id, extracted2); - } - if (extracted2?.methods?.length) { - const defLine2 = sigNode.startPosition.row + 1; - info2 = extracted2.methods.find( - (m) => m.name === customResult.funcName && m.line === defLine2, - ); - if (info2) { - arity2 = info2.parameters.some((p) => p.isVariadic) - ? undefined - : info2.parameters.length; - } - if (arity2 !== undefined && info2) { - const methodMap = new Map(); - for (const m of extracted2.methods) methodMap.set(`${m.name}:${m.line}`, m); - const groups2 = buildCollisionGroups(methodMap); - encTypeTag2 = - typeTagForId( - methodMap, - customResult.funcName, - arity2, - info2, - language2, - groups2, - ) + constTagForId(methodMap, customResult.funcName, arity2, info2, groups2); - } - } - } - if (!info2 && provider.methodExtractor.extractFromNode) { - const nodeInfo = provider.methodExtractor.extractFromNode(sigNode, { - filePath, - language: language2, - }); - if (nodeInfo) { - arity2 = nodeInfo.parameters.some((p) => p.isVariadic) - ? undefined - : nodeInfo.parameters.length; - } - } - } - const arityTag2 = arity2 !== undefined ? `#${arity2}${encTypeTag2}` : ''; - return generateId(finalLabel, `${filePath}:${qualifiedName}${arityTag2}`); - } - } - - current = current.parent; - } - - return null; -}; - -/** - * Verify constructor bindings against SymbolTable and infer receiver types. - * Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths. - */ -const verifyConstructorBindings = ( - bindings: readonly ConstructorBinding[], - filePath: string, - ctx: ResolutionContext, - graph?: KnowledgeGraph, - bindingAccumulator?: BindingAccumulator, -): Map => { - const verified = new Map(); - - for (const { scope, varName, calleeName, receiverClassName } of bindings) { - const tiered = ctx.resolve(calleeName, filePath); - const isClass = tiered?.candidates.some((def) => def.type === 'Class') ?? false; - - if (isClass) { - verified.set(receiverKey(scope, varName), calleeName); - } else { - let callableDefs = tiered?.candidates.filter( - (d) => d.type === 'Function' || d.type === 'Method', - ); - - // When receiver class is known (e.g. $this->method() in PHP), narrow - // candidates to methods owned by that class to avoid false disambiguation failures. - if (callableDefs && callableDefs.length > 1 && receiverClassName) { - if (graph) { - // Worker path: use graph.getNode (fast, already in-memory) - const narrowed = callableDefs.filter((d) => { - if (!d.ownerId) return false; - const owner = graph.getNode(d.ownerId); - return owner?.properties.name === receiverClassName; - }); - if (narrowed.length > 0) callableDefs = narrowed; - } else { - // Sequential path: use ctx.resolve (no graph available) - const classResolved = ctx.resolve(receiverClassName, filePath); - if (classResolved && classResolved.candidates.length > 0) { - const classNodeIds = new Set(classResolved.candidates.map((c) => c.nodeId)); - const narrowed = callableDefs.filter((d) => d.ownerId && classNodeIds.has(d.ownerId)); - if (narrowed.length > 0) callableDefs = narrowed; - } - } - } - - let typeName: string | undefined; - if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) { - typeName = extractReturnTypeName(callableDefs[0].returnType); - } - - // Phase 9: BindingAccumulator fallback for cross-file return types. - // Used when the SymbolTable has no return type for a cross-file callee - // (e.g., a return type that TypeEnv resolved via fixpoint in the source - // file but was not stored as a SymbolTable returnType annotation). - // namedImportMap tells us which source file exported the callee so we - // can look up its file-scope binding via the O(1) fileScopeGet method. - // - // Tier gating: only fall back to the accumulator when resolution is - // unambiguously import-scoped or global. When tiered.tier is 'same-file', - // the local definition is authoritative even without a return type - // annotation — using the accumulator here would let an imported callee - // with the same name shadow the local one, producing false CALLS edges. - // When multiple callable candidates exist, the accumulator would pick - // arbitrarily — skip to avoid fabricated edges. - // - // Quality note: worker-path accumulator entries are Tier 0/1 only - // (annotation-declared + same-file constructor inference) — see the - // BindingAccumulator class JSDoc. For large repos where the worker - // path dominates, Phase 9 binding accuracy is structurally lower - // than for sequential-path repos where Tier 2 cross-file propagation - // is available. - // - // Overlapping mechanism note: this is one of three cross-file - // return-type resolution paths in the codebase: - // 1. buildImportedReturnTypes (~line 109) — namedImportMap + - // SymbolTable.lookupExactFull (structure-processor captured) - // 2. collectExportedBindings (~line 168) / enrichExportedTypeMap - // — TypeEnv + graph isExported flag - // 3. This fallback — namedImportMap + BindingAccumulator - // A future cleanup should merge these into a single resolution pass. - const shouldFallback = - tiered?.tier !== 'same-file' && (!callableDefs || callableDefs.length <= 1); - if (!typeName && bindingAccumulator && shouldFallback) { - const namedImports = ctx.namedImportMap.get(filePath); - const importBinding = namedImports?.get(calleeName); - if (importBinding) { - const rawType = bindingAccumulator.fileScopeGet( - importBinding.sourcePath, - importBinding.exportedName, - ); - if (rawType) { - typeName = extractReturnTypeName(rawType); - } - } - } - - if (typeName) { - verified.set(receiverKey(scope, varName), typeName); - } - } - } - - return verified; -}; - -/** - * Resolution result with confidence scoring - */ -interface ResolveResult { - nodeId: string; - confidence: number; - reason: string; - returnType?: string; -} - -/** - * After resolving a call to an interface method, find additional targets - * in classes implementing that interface. Returns implementation method - * results with lower confidence ('interface-dispatch'). - */ -function findInterfaceDispatchTargets( - calledName: string, - receiverTypeName: string, - currentFile: string, - ctx: ResolutionContext, - heritageMap: HeritageMap, - primaryNodeId: string, -): ResolveResult[] { - const implFiles = heritageMap.getImplementorFiles(receiverTypeName); - if (implFiles.size === 0) return []; - - const typeResolved = ctx.resolve(receiverTypeName, currentFile); - if (!typeResolved) return []; - if (!typeResolved.candidates.some((c) => c.type === 'Interface')) return []; - - const results: ResolveResult[] = []; - for (const implFile of implFiles) { - const methods = ctx.model.symbols.lookupExactAll(implFile, calledName); - for (const method of methods) { - if (method.nodeId !== primaryNodeId) { - results.push({ - nodeId: method.nodeId, - confidence: 0.7, - reason: 'interface-dispatch', - }); - } - } - } - return results; -} - -export const processCalls = async ( - graph: KnowledgeGraph, - files: { path: string; content: string }[], - astCache: ASTCache, - ctx: ResolutionContext, - onProgress?: (current: number, total: number) => void, - exportedTypeMap?: ExportedTypeMap, - /** Phase 14: pre-resolved cross-file bindings to seed into buildTypeEnv. Keyed by filePath → Map. */ - importedBindingsMap?: ReadonlyMap>, - /** Phase 14 E3: cross-file return types for imported callables. Keyed by filePath → Map. - * Consulted ONLY when SymbolTable has no unambiguous match (local-first principle). */ - importedReturnTypesMap?: ReadonlyMap>, - /** Phase 14 E3: cross-file RAW return types for for-loop element extraction. Keyed by filePath → Map. */ - importedRawReturnTypesMap?: ReadonlyMap>, - heritageMap?: HeritageMap, - bindingAccumulator?: BindingAccumulator, - /** - * Optional cache for compiled `Parser.Query` objects keyed by language name. - * When provided, compiled queries are reused across calls instead of being - * re-compiled from the query string for every file. Callers that invoke - * `processCalls` many times with single-file batches (e.g. the cross-file - * propagation phase) should pass a long-lived map here to avoid O(N) - * query recompilation overhead. - */ - compiledQueryCache?: Map, -): Promise => { - const parser = await loadParser(); - const collectedHeritage: ExtractedHeritage[] = []; - const pendingWrites: { - receiverTypeName: string; - propertyName: string; - filePath: string; - srcId: string; - line?: number; - }[] = []; - // Phase P cross-file: accumulate heritage across files for cross-file isSubclassOf. - // Used as a secondary check when per-file parentMap lacks the relationship — helps - // when the heritage-declaring file is processed before the call site file. - // For remaining cases (reverse file order), the SymbolTable class-type fallback applies. - const globalParentMap = new Map(); - const globalParentSeen = new Map>(); - const logSkipped = isVerboseIngestionEnabled(); - const skippedByLang = logSkipped ? new Map() : null; - - // ── Prepare-then-resolve: single preparation loop, deferred resolution ── - // All files are prepared (parse → query → heritage → TypeEnv) in one loop, - // then resolved (verifyConstructorBindings → call edges) in a second loop. - // This ensures: - // 1. When bindingAccumulator is present, ALL files flush their TypeEnv - // bindings before ANY verifyConstructorBindings reads — fixing the - // consumer-before-provider ordering bug on the sequential path. - // 2. globalParentMap is fully populated before resolution, improving - // cross-file isSubclassOf accuracy regardless of file order. - // For the sequential path (<15 files), buffering per-file state is negligible. - interface PreparedFile { - file: { path: string; content: string }; - language: SupportedLanguages; - provider: ReturnType; - tree: ReturnType; - matches: ReturnType; - parentMap: ReadonlyMap; - typeEnv: ReturnType; - } - const prepared: PreparedFile[] = []; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (i % 20 === 0) await yieldToEventLoop(); - - const language = getLanguageFromFilename(file.path); - if (!language) continue; - // Registry-primary gate: scope-based phase owns CALLS for this lang. - if (isRegistryPrimary(language)) continue; - if (!isLanguageAvailable(language)) { - if (skippedByLang) { - skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1); - } - continue; - } - - const provider = getProvider(language); - const queryStr = provider.treeSitterQueries; - if (!queryStr) continue; - - await loadLanguage(language, file.path); - - let tree = astCache.get(file.path); - if (!tree) { - const parseContent = provider.preprocessSource?.(file.content, file.path) ?? file.content; - try { - tree = parseSourceSafe(parser, parseContent, undefined, { - bufferSize: getTreeSitterBufferSize(parseContent), - }); - } catch (parseError) { - continue; - } - astCache.set(file.path, tree); - } - - let matches; - try { - const lang = parser.getLanguage(); - let query = compiledQueryCache?.get(language); - if (!query) { - query = new Parser.Query(lang, queryStr); - compiledQueryCache?.set(language, query); - } - matches = query.matches(tree.rootNode); - } catch (queryError) { - logger.warn({ queryError }, `Query error for ${file.path}:`); - continue; - } - - // Extract heritage from query matches to build parentMap for buildTypeEnv. - // Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs. - const fileParentMap = new Map(); - if (provider.heritageExtractor) { - for (const match of matches) { - const captureMap: Record = {}; - match.captures.forEach((c) => (captureMap[c.name] = c.node)); - if (captureMap['heritage.class']) { - const heritageItems = provider.heritageExtractor.extract(captureMap, { - filePath: file.path, - language, - }); - for (const item of heritageItems) { - if (item.kind === 'extends') { - let parents = fileParentMap.get(item.className); - if (!parents) { - parents = []; - fileParentMap.set(item.className, parents); - } - if (!parents.includes(item.parentName)) parents.push(item.parentName); - } - } - } - } - } - const parentMap: ReadonlyMap = fileParentMap; - // Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups. - for (const [cls, parents] of fileParentMap) { - let global = globalParentMap.get(cls); - let seen = globalParentSeen.get(cls); - if (!global) { - global = []; - globalParentMap.set(cls, global); - } - if (!seen) { - seen = new Set(); - globalParentSeen.set(cls, seen); - } - for (const p of parents) { - if (!seen.has(p)) { - seen.add(p); - global.push(p); - } - } - } - - const importedBindings = importedBindingsMap?.get(file.path); - const importedReturnTypes = importedReturnTypesMap?.get(file.path); - const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path); - const typeEnv = buildTypeEnv(tree, language, { - filePath: file.path, - model: ctx.model, - parentMap, - importedBindings, - importedReturnTypes, - importedRawReturnTypes, - enclosingFunctionFinder: provider?.enclosingFunctionFinder, - extractFunctionName: provider?.methodExtractor?.extractFunctionName, - }); - if (typeEnv && exportedTypeMap) { - const fileExports = collectExportedBindings(typeEnv, file.path, ctx.model.symbols, graph); - if (fileExports) exportedTypeMap.set(file.path, fileExports); - } - if (bindingAccumulator) { - typeEnv.flush(file.path, bindingAccumulator); - } - - prepared.push({ file, language, provider, tree, matches, parentMap, typeEnv }); - } - - // ── Property-registration pre-pass ── - // Register all routed properties (e.g. Ruby attr_accessor) BEFORE the - // resolution loop so cross-file field-type lookups (e.g. - // `user.address.save → Address#save`) succeed regardless of file - // processing order. This MUST stay in lockstep with the equivalent - // worker-path block in parse-worker.ts (kind === 'properties') — any - // divergence between the two paths breaks the `incremental ≡ --force` - // invariant once a repo crosses the worker threshold between runs. - const fieldInfoCache = new Map>(); - for (const { file, language, provider, matches, typeEnv } of prepared) { - const callRouter = provider.callRouter; - if (!callRouter) continue; - matches.forEach((match) => { - const captureMap: Record = {}; - match.captures.forEach((c) => (captureMap[c.name] = c.node)); - if (!captureMap['call']) return; - const callNameNode = captureMap['call.name']; - if (!callNameNode) return; - const routed = callRouter(callNameNode.text, captureMap['call']); - if (!routed || routed.kind !== 'properties') return; - - // #1978: thread the qualifier so a routed property's owner edge points at - // the *qualified* nested-class node (Shapes.Circle) instead of a now-nonexistent - // simple `Class:file:Circle` id. Gated on the flag → byte-identical when off. - // MUST stay in lockstep with the worker `kind === 'properties'` block. - const propGetQualifiedOwnerName = - provider.classExtractor?.qualifiedNodeId === true - ? (node: SyntaxNode, simpleName: string): string | null => - provider.classExtractor!.extractQualifiedName(node, simpleName) - : undefined; - const propEnclosingInfo = findEnclosingClassInfo( - captureMap['call'], - file.path, - provider.resolveEnclosingOwner, - propGetQualifiedOwnerName, - ); - const propEnclosingClassId = - propEnclosingInfo?.qualifiedClassId ?? propEnclosingInfo?.classId ?? null; - - // Enrich routed properties with FieldExtractor metadata so types - // discovered from constructor assignments (e.g. `@address = Address.new`) - // are propagated even when the routing payload itself lacks declaredType. - let routedFieldMap: Map | undefined; - if (provider.fieldExtractor && typeEnv) { - const classNode = findEnclosingClassNode(captureMap['call']); - if (classNode) { - routedFieldMap = getFieldInfo( - classNode, - provider, - { - typeEnv, - symbolTable: NOOP_SYMBOL_TABLE, - filePath: file.path, - language, - }, - fieldInfoCache, - ); - } - } - - const fileId = generateId('File', file.path); - for (const item of routed.items) { - const routedFieldInfo = routedFieldMap?.get(item.propName); - const propQualifiedName = propEnclosingInfo - ? `${propEnclosingInfo.className}.${item.propName}` - : item.propName; - const nodeId = generateId('Property', `${file.path}:${propQualifiedName}`); - graph.addNode({ - id: nodeId, - label: 'Property', - properties: { - name: item.propName, - filePath: file.path, - startLine: item.startLine, - endLine: item.endLine, - language, - isExported: true, - description: item.accessorType, - ...(item.declaredType - ? { declaredType: item.declaredType } - : routedFieldInfo?.type - ? { declaredType: routedFieldInfo.type } - : {}), - ...(routedFieldInfo?.visibility !== undefined - ? { visibility: routedFieldInfo.visibility } - : {}), - ...(routedFieldInfo?.isStatic !== undefined - ? { isStatic: routedFieldInfo.isStatic } - : {}), - ...(routedFieldInfo?.isReadonly !== undefined - ? { isReadonly: routedFieldInfo.isReadonly } - : {}), - }, - }); - ctx.model.symbols.add(file.path, item.propName, nodeId, 'Property', { - ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}), - ...(item.declaredType - ? { declaredType: item.declaredType } - : routedFieldInfo?.type - ? { declaredType: routedFieldInfo.type } - : {}), - }); - // Only emit File -> Property DEFINES for top-level properties (issue #1944). - if (!propEnclosingClassId) { - const relId = generateId('DEFINES', `${fileId}->${nodeId}`); - graph.addRelationship({ - id: relId, - sourceId: fileId, - targetId: nodeId, - type: 'DEFINES', - confidence: 1.0, - reason: '', - }); - } - if (propEnclosingClassId) { - graph.addRelationship({ - id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`), - sourceId: propEnclosingClassId, - targetId: nodeId, - type: 'HAS_PROPERTY', - confidence: 1.0, - reason: '', - }); - } - } - }); - } - - // ── Resolution loop: verify constructor bindings and resolve calls ── - // The accumulator (if present) is now fully populated from the preparation - // loop above, so verifyConstructorBindings sees all provider bindings - // regardless of file processing order. - for (let i = 0; i < prepared.length; i++) { - const { file, language, provider, tree, matches, parentMap, typeEnv } = prepared[i]; - - enclosingFnExtractCache.clear(); - onProgress?.(i + 1, files.length); - if (i % 20 === 0) await yieldToEventLoop(); - - const callRouter = provider.callRouter; - - const verifiedReceivers = - typeEnv.constructorBindings.length > 0 - ? verifyConstructorBindings( - typeEnv.constructorBindings, - file.path, - ctx, - undefined, // graph not available on the sequential path here - bindingAccumulator, // Phase 9 fallback — same as worker path (R3 parity) - ) - : new Map(); - const receiverIndex = buildReceiverTypeIndex(verifiedReceivers); - - ctx.enableCache(file.path); - const widenCache: WidenCache = new Map(); - - matches.forEach((match) => { - const captureMap: Record = {}; - match.captures.forEach((c) => (captureMap[c.name] = c.node)); - // ── Write access: emit ACCESSES {reason: 'write'} for assignments to member fields ── - if ( - captureMap['assignment'] && - captureMap['assignment.receiver'] && - captureMap['assignment.property'] - ) { - const receiverNode = captureMap['assignment.receiver']; - const propertyName: string = captureMap['assignment.property'].text; - // Resolve receiver type: simple identifier → TypeEnv lookup or class resolution - let receiverTypeName: string | undefined; - const receiverText = receiverNode.text; - if (receiverText && typeEnv) { - receiverTypeName = typeEnv.lookup(receiverText, captureMap['assignment']); - } - // Fall back to verified constructor bindings (mirrors CALLS resolution tier 2) - if (!receiverTypeName && receiverText && receiverIndex.size > 0) { - const enclosing = findEnclosingFunction( - captureMap['assignment'], - file.path, - ctx, - provider, - ); - const funcName = enclosing ? extractFuncNameFromSourceId(enclosing) : ''; - receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverText); - } - if (!receiverTypeName && receiverText) { - const resolved = ctx.resolve(receiverText, file.path); - if (resolved?.candidates.some((d) => CLASS_LIKE_TYPES.has(d.type))) { - receiverTypeName = receiverText; - } - } - if (receiverTypeName) { - const enclosing = findEnclosingFunction( - captureMap['assignment'], - file.path, - ctx, - provider, - ); - const srcId = enclosing || generateId('File', file.path); - // Defer resolution: Ruby attr_accessor properties are registered during - // this same loop, so cross-file lookups fail if the declaring file hasn't - // been processed yet. Collect now, resolve after all files are done. - pendingWrites.push({ - receiverTypeName, - propertyName, - filePath: file.path, - srcId, - line: captureMap['assignment'].startPosition.row + 1, - }); - } - // Assignment-only capture (no @call sibling): skip the rest of this - // forEach iteration — this acts as a `continue` in the match loop. - if (!captureMap['call']) return; - } - - if (!captureMap['call']) return; - - const callNode = captureMap['call']; - const callExtractor = provider.callExtractor; - - // ── Language-specific call site (e.g. Java :: method references) ── - if (callExtractor) { - const langCallSite = callExtractor.extract(callNode, undefined); - if (langCallSite) { - if (provider.isBuiltInName(langCallSite.calledName)) return; - - const sourceId = - findEnclosingFunction(callNode, file.path, ctx, provider) || - generateId('File', file.path); - const receiverName = - langCallSite.callForm === 'member' ? langCallSite.receiverName : undefined; - let receiverTypeName = - receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined; - - if ( - langCallSite.typeAsReceiverHeuristic && - receiverName !== undefined && - receiverTypeName === undefined && - langCallSite.callForm === 'member' - ) { - const c0 = receiverName.charCodeAt(0); - if (c0 >= 65 && c0 <= 90) receiverTypeName = receiverName; - } - - const resolved = resolveCallTarget( - { - calledName: langCallSite.calledName, - callForm: langCallSite.callForm, - ...(receiverTypeName !== undefined ? { receiverTypeName } : {}), - ...(receiverName !== undefined ? { receiverName } : {}), - }, - file.path, - ctx, - undefined, - widenCache, - undefined, - heritageMap, - ); - - if (!resolved) return; - graph.addRelationship({ - id: generateId('CALLS', `${sourceId}:${langCallSite.calledName}->${resolved.nodeId}`), - sourceId, - targetId: resolved.nodeId, - type: 'CALLS', - confidence: resolved.confidence, - reason: resolved.reason, - }); - - if (heritageMap && langCallSite.callForm === 'member' && receiverTypeName) { - const implTargets = findInterfaceDispatchTargets( - langCallSite.calledName, - receiverTypeName, - file.path, - ctx, - heritageMap, - resolved.nodeId, - ); - for (const impl of implTargets) { - graph.addRelationship({ - id: generateId('CALLS', `${sourceId}:${langCallSite.calledName}->${impl.nodeId}`), - sourceId, - targetId: impl.nodeId, - type: 'CALLS', - confidence: impl.confidence, - reason: impl.reason, - }); - } - } - return; - } - } - - const nameNode = captureMap['call.name']; - if (!nameNode) return; - - const calledName = nameNode.text; - - // Check heritage extractor for call-based heritage (e.g., Ruby include/extend/prepend) - if (provider.heritageExtractor?.extractFromCall) { - const heritageItems = provider.heritageExtractor.extractFromCall( - calledName, - captureMap['call'], - { filePath: file.path, language }, - ); - if (heritageItems !== null) { - for (const item of heritageItems) { - collectedHeritage.push({ - filePath: file.path, - className: item.className, - parentName: item.parentName, - kind: item.kind, - }); - } - return; - } - } - - // Dispatch: route language-specific calls (properties, imports) - // Heritage routing is handled by heritageExtractor.extractFromCall above. - const routed = callRouter?.(calledName, captureMap['call']); - if (routed) { - switch (routed.kind) { - case 'skip': - case 'import': - return; - - case 'properties': { - // Properties already registered in the pre-pass above. - // Skip to avoid duplicate nodes/edges. - return; - } - - case 'call': - break; - } - } - - if (provider.isBuiltInName(calledName)) return; - - // --- DAG stage 2-3: classify-form + infer-receiver (shared defaults) --- - // These stages run the shared inference chain. Language providers can - // customize infer-receiver (stage 3) via the inferImplicitReceiver hook - // which runs AFTER this default chain (typed-binding → constructor-map → - // module-alias → class-as-receiver → mixed-chain), and selectDispatch - // (stage 4) which picks the resolver branch. - let callForm = inferCallForm(callNode, nameNode); - let receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined; - let receiverTypeName = - receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined; - let receiverSource: ReceiverSource = receiverTypeName ? 'typed-binding' : 'none'; - // Phase P: virtual dispatch override — when the declared type is a base class but - // the constructor created a known subclass, prefer the more specific type. - // Checks per-file parentMap first, then falls back to globalParentMap for - // cross-file heritage (e.g. Dog extends Animal declared in a different file). - // Reconstructs the exact scope key (funcName@startIndex\0varName) from the - // enclosing function AST node for a correct, O(1) map lookup. - if (receiverTypeName && receiverName && typeEnv && typeEnv.constructorTypeMap.size > 0) { - // Reconstruct scope key to match constructorTypeMap's scope\0varName format - let scope = ''; - let p = callNode.parent; - while (p) { - if (FUNCTION_NODE_TYPES.has(p.type)) { - const funcName = - provider.methodExtractor?.extractFunctionName?.(p, file.path)?.funcName ?? - genericFuncName(p); - if (funcName) { - scope = `${funcName}@${p.startIndex}`; - break; - } - } - p = p.parent; - } - const ctorType = typeEnv.constructorTypeMap.get(`${scope}\0${receiverName}`); - if (ctorType && ctorType !== receiverTypeName) { - // Verify subclass relationship: per-file parentMap first, then cross-file - // globalParentMap, then fall back to SymbolTable class verification. - // The SymbolTable fallback handles cross-file cases where heritage is declared - // in a file not yet processed (e.g. Dog extends Animal in models/Dog.kt when - // processing services/App.kt). Since constructorTypeMap only records entries - // when a type annotation AND constructor are both present (val x: Base = Sub()), - // confirming both are class-like types is sufficient — the original code would - // not compile if Sub didn't extend Base. - if ( - isSubclassOf(ctorType, receiverTypeName, parentMap) || - isSubclassOf(ctorType, receiverTypeName, globalParentMap) || - (ctx.model.types.lookupClassByName(ctorType).length > 0 && - ctx.model.types.lookupClassByName(receiverTypeName).length > 0) - ) { - receiverTypeName = ctorType; - receiverSource = 'constructor-map'; - } - } - } - // Fall back to verified constructor bindings for return type inference - if (!receiverTypeName && receiverName && receiverIndex.size > 0) { - const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider); - const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : ''; - receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName); - if (receiverTypeName) receiverSource = 'constructor-map'; - } - // Fall back to class-as-receiver for static method calls (e.g. UserService.find_user(), - // Greetable.format()). When the receiver name is not a variable in TypeEnv but - // resolves to a class-like symbol (Class / Interface / Struct / Enum / Trait) via - // tiered resolution, use it directly as the receiver type. `Trait` is included so - // Ruby module class-method calls flow through the class-as-receiver path and reach - // the `selectDispatch` hook's singleton branch. - if (!receiverTypeName && receiverName && callForm === 'member') { - const typeResolved = ctx.resolve(receiverName, file.path); - if ( - typeResolved && - typeResolved.candidates.some( - (d) => - d.type === 'Class' || - d.type === 'Interface' || - d.type === 'Struct' || - d.type === 'Enum' || - d.type === 'Trait', - ) - ) { - receiverTypeName = receiverName; - receiverSource = 'class-as-receiver'; - } - } - // Hoist sourceId so it's available for ACCESSES edge emission during chain walk. - const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx, provider); - const sourceId = enclosingFuncId || generateId('File', file.path); - - // Fall back to mixed chain resolution when the receiver is a complex expression - // (field chain, call chain, or interleaved — e.g. user.address.city.save() or - // svc.getUser().address.save()). Handles all cases with a single unified walk. - if (callForm === 'member' && !receiverTypeName && !receiverName) { - const receiverNode = extractReceiverNode(nameNode); - if (receiverNode) { - const extracted = extractMixedChain(receiverNode); - if (extracted && extracted.chain.length > 0) { - let currentType = - extracted.baseReceiverName && typeEnv - ? typeEnv.lookup(extracted.baseReceiverName, callNode) - : undefined; - if (!currentType && extracted.baseReceiverName && receiverIndex.size > 0) { - const funcName = enclosingFuncId ? extractFuncNameFromSourceId(enclosingFuncId) : ''; - currentType = lookupReceiverType(receiverIndex, funcName, extracted.baseReceiverName); - } - if (!currentType && extracted.baseReceiverName) { - const cr = ctx.resolve(extracted.baseReceiverName, file.path); - if ( - cr?.candidates.some( - (d) => - d.type === 'Class' || - d.type === 'Interface' || - d.type === 'Struct' || - d.type === 'Enum', - ) - ) { - currentType = extracted.baseReceiverName; - } - } - if (currentType) { - receiverTypeName = walkMixedChain( - extracted.chain, - currentType, - file.path, - ctx, - makeAccessEmitter(graph, sourceId), - heritageMap, - ); - if (receiverTypeName) receiverSource = 'mixed-chain'; - } - } - } - } - - // --- DAG stage 3: infer-receiver (provider hook) --- - // Synthesize implicit receivers for languages that omit them (e.g., Ruby bare-call). - // This hook runs AFTER the shared inference chain so explicit receivers / - // typed bindings always take precedence. Output (if non-null) overlays onto - // the ReceiverEnriched for the next stage. - let dispatchHint: string | undefined; - if (provider.inferImplicitReceiver) { - const override = provider.inferImplicitReceiver({ - calledName, - callForm, - receiverName, - receiverTypeName, - callNode, - filePath: file.path, - }); - if (override) { - callForm = override.callForm; - receiverName = override.receiverName; - receiverTypeName = override.receiverTypeName; - receiverSource = override.receiverSource; - dispatchHint = override.hint; - } - } - - // --- DAG stage 4: select-dispatch (provider hook + default fallback) --- - // Decide which resolver path to try first (primary) and fallback strategy. - // Language providers can customize dispatch via selectDispatch hook; all - // others use the shared defaultDispatchDecision. Always non-null after this - // block so downstream resolvers are table-driven. - const dispatchDecision: DispatchDecision = - provider.selectDispatch?.({ - calledName, - callForm, - receiverName, - receiverTypeName, - receiverSource, - hint: dispatchHint, - }) ?? defaultDispatchDecision(callForm); - - // Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++). - // Only used when multiple candidates survive arity filtering — ~1-3% of calls. - const langConfig = provider.typeConfig; - const hints: OverloadHints | undefined = langConfig?.inferLiteralType - ? { callNode, inferLiteralType: langConfig.inferLiteralType, typeEnv } - : undefined; - - const resolved = resolveCallTarget( - { - calledName, - argCount: countCallArguments(callNode), - callForm, - receiverTypeName, - receiverName, - }, - file.path, - ctx, - hints, - widenCache, - undefined, - heritageMap, - dispatchDecision, - ); - - if (!resolved) return; - const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`); - - graph.addRelationship({ - id: relId, - sourceId, - targetId: resolved.nodeId, - type: 'CALLS', - confidence: resolved.confidence, - reason: resolved.reason, - }); - - if (heritageMap && callForm === 'member' && receiverTypeName) { - const implTargets = findInterfaceDispatchTargets( - calledName, - receiverTypeName, - file.path, - ctx, - heritageMap, - resolved.nodeId, - ); - for (const impl of implTargets) { - graph.addRelationship({ - id: generateId('CALLS', `${sourceId}:${calledName}->${impl.nodeId}`), - sourceId, - targetId: impl.nodeId, - type: 'CALLS', - confidence: impl.confidence, - reason: impl.reason, - }); - } - } - }); - - // Vue: emit CALLS edges for PascalCase components used in