mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
Merge branch 'abhigyanpatwari:main' into feat/nuxt-auto-imports
This commit is contained in:
commit
bcebe9494f
234 changed files with 4536 additions and 23793 deletions
4
.github/CODEOWNERS
vendored
Normal file
4
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Code owners
|
||||
|
||||
* @Arvuno
|
||||
* @magyargergo
|
||||
87
.github/workflows/ci-scope-parity.yml
vendored
87
.github/workflows/ci-scope-parity.yml
vendored
|
|
@ -1,87 +0,0 @@
|
|||
name: Scope Resolution Parity
|
||||
|
||||
# Reusable workflow — called from ci.yml. Does NOT declare concurrency;
|
||||
# it inherits the caller's concurrency group per the convention documented
|
||||
# in CONTRIBUTING.md → "GitHub Actions — Concurrency Convention".
|
||||
#
|
||||
# ── Purpose (RFC #909 Ring 3, §6.4 "Observability gates") ──────────────
|
||||
# For every language in `MIGRATED_LANGUAGES` (exported from
|
||||
# `gitnexus/src/core/ingestion/registry-primary-flag.ts`), run the
|
||||
# resolver integration test at `test/integration/resolvers/<slug>.test.ts`
|
||||
# TWICE on every PR:
|
||||
#
|
||||
# 1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path (guarantees we haven't
|
||||
# broken the old path while migrating). Known legacy gaps may be skipped
|
||||
# through the resolver test helper's expected-failure list.
|
||||
# 2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path (guarantees the
|
||||
# new path carries the same behavior — the parity gate).
|
||||
#
|
||||
# BOTH must pass. The source of truth is the TypeScript constant — adding
|
||||
# a language to that `Set` is the ONLY contributor action; CI auto-
|
||||
# discovers it, runs parity, and the language's default production path
|
||||
# flips to registry-primary in the same change.
|
||||
#
|
||||
# When the set is empty (e.g. mid-Ring-3 for every language), the parity
|
||||
# matrix is skipped and the workflow reports success — no-op until a
|
||||
# language is explicitly claimed migrated.
|
||||
#
|
||||
# ── Consolidation (chore/vitest-speed-strategy) ────────────────────────
|
||||
# Previously each language was a separate GitHub Actions matrix job,
|
||||
# meaning N languages × 1 checkout+install+build per shard. The build
|
||||
# cost dwarfed the test cost (~5 min setup for ~15 sec test execution).
|
||||
#
|
||||
# Now a single job runs `scripts/run-parity.ts` which loops through all
|
||||
# migrated languages sequentially (2 vitest invocations per language:
|
||||
# legacy + registry-primary). All failures are collected and reported
|
||||
# at the end (equivalent to the old fail-fast: false behavior).
|
||||
#
|
||||
# Adding a new language to MIGRATED_LANGUAGES still requires no workflow
|
||||
# edit — the script auto-discovers the set at runtime.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
discover:
|
||||
name: Discover migrated languages
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
has-any: ${{ steps.read.outputs.has-any }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-gitnexus
|
||||
|
||||
- name: Extract MIGRATED_LANGUAGES from registry-primary-flag.ts
|
||||
id: read
|
||||
shell: bash
|
||||
working-directory: gitnexus
|
||||
run: |
|
||||
set -euo pipefail
|
||||
LANGS=$(npx tsx scripts/ci-list-migrated-languages.ts)
|
||||
COUNT=$(printf '%s' "$LANGS" | jq 'length')
|
||||
HAS_ANY="false"
|
||||
if [[ "$COUNT" -gt 0 ]]; then HAS_ANY="true"; fi
|
||||
echo "has-any=$HAS_ANY" >> "$GITHUB_OUTPUT"
|
||||
echo "Discovered $COUNT migrated language(s): $LANGS"
|
||||
echo "Parity will run: $HAS_ANY"
|
||||
|
||||
parity:
|
||||
name: scope-resolution parity
|
||||
needs: discover
|
||||
if: needs.discover.outputs.has-any == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/setup-gitnexus
|
||||
with:
|
||||
build: 'true'
|
||||
|
||||
- name: Run parity for all migrated languages
|
||||
shell: bash
|
||||
working-directory: gitnexus
|
||||
run: npx tsx scripts/run-parity.ts
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
|
|
@ -27,9 +27,8 @@ concurrency:
|
|||
# Each concern lives in its own workflow file for maintainability:
|
||||
# ci-quality.yml — typecheck (tsc --noEmit)
|
||||
# ci-tests.yml — unit + integration tests with coverage + cross-platform
|
||||
# (includes the scope-resolution resolver tests)
|
||||
# ci-e2e.yml — E2E tests (only when gitnexus-web/ changes)
|
||||
# ci-scope-parity.yml — RFC #909 Ring 3 parity gate: legacy DAG + registry-primary
|
||||
# both pass, per migrated language in the JSON registry
|
||||
#
|
||||
# Shared setup is DRY via .github/actions/setup-gitnexus composite action.
|
||||
|
||||
|
|
@ -49,11 +48,6 @@ jobs:
|
|||
permissions:
|
||||
contents: read
|
||||
|
||||
scope-parity:
|
||||
uses: ./.github/workflows/ci-scope-parity.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# ── Save PR metadata for the reporting workflow ─────────────────
|
||||
# The ci-report.yml workflow (triggered by workflow_run) needs the
|
||||
# PR number and job results to post a comment. We save them as an
|
||||
|
|
@ -62,7 +56,7 @@ jobs:
|
|||
save-pr-meta:
|
||||
name: Save PR Metadata
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
needs: [quality, tests, e2e, scope-parity]
|
||||
needs: [quality, tests, e2e]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
|
|
@ -73,14 +67,12 @@ jobs:
|
|||
QUALITY: ${{ needs.quality.result }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
E2E: ${{ needs.e2e.result }}
|
||||
SCOPE_PARITY: ${{ needs.scope-parity.result }}
|
||||
run: |
|
||||
mkdir -p pr-meta
|
||||
echo "$PR_NUMBER" > pr-meta/pr_number
|
||||
echo "$QUALITY" > pr-meta/quality_result
|
||||
echo "$TESTS" > pr-meta/tests_result
|
||||
echo "$E2E" > pr-meta/e2e_result
|
||||
echo "$SCOPE_PARITY" > pr-meta/scope_parity_result
|
||||
# TODO(post-merge): remove backward-compat copies once ci-report.yml
|
||||
# on main reads underscore names.
|
||||
# Backward-compat: ci-report.yml on main still reads hyphenated
|
||||
|
|
@ -103,7 +95,7 @@ jobs:
|
|||
# Single required check for branch protection.
|
||||
ci-status:
|
||||
name: CI Gate
|
||||
needs: [quality, tests, e2e, scope-parity]
|
||||
needs: [quality, tests, e2e]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
|
@ -117,14 +109,15 @@ jobs:
|
|||
# reusable workflow, so `needs.tests.result` below blocks the merge
|
||||
# on an ABI mismatch. (`jobs.<id>.result` cannot be exposed as a
|
||||
# workflow_call output, so the gate is enforced transitively here.)
|
||||
# The scope-resolution resolver tests also run inside the `tests`
|
||||
# workflow (RING4-1 #942 removed the separate scope-parity gate),
|
||||
# so a resolver regression makes TESTS != success and blocks here.
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
E2E: ${{ needs.e2e.result }}
|
||||
SCOPE_PARITY: ${{ needs.scope-parity.result }}
|
||||
run: |
|
||||
echo "Quality: $QUALITY"
|
||||
echo "Tests: $TESTS"
|
||||
echo "E2E: $E2E"
|
||||
echo "Scope parity: $SCOPE_PARITY"
|
||||
# A failed `abi-assert` job (#1922) inside the tests reusable
|
||||
# workflow makes TESTS != success, so this clause also blocks the
|
||||
# merge on a tree-sitter ABI mismatch.
|
||||
|
|
@ -137,14 +130,3 @@ jobs:
|
|||
echo "::error::E2E job failed"
|
||||
exit 1
|
||||
fi
|
||||
# scope-parity is a reusable workflow. With an empty migrated-
|
||||
# languages list, its parity matrix is skipped and the outer
|
||||
# workflow still reports `success`. If any entry's legacy-DAG or
|
||||
# registry-primary run fails, the workflow reports `failure`.
|
||||
# Accept only `success`; `skipped` would mean the entire
|
||||
# discover job was skipped too (upstream failure), which should
|
||||
# still block.
|
||||
if [[ "$SCOPE_PARITY" != "success" ]]; then
|
||||
echo "::error::Scope-resolution parity gate failed (RFC #909 Ring 3)"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
|
@ -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 }}'
|
||||
|
|
|
|||
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
|
|
@ -141,14 +141,14 @@ jobs:
|
|||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
|
@ -163,7 +163,7 @@ jobs:
|
|||
# `akonlabs/gitnexus` and `akonlabs/gitnexus-web` repos.
|
||||
- name: Log in to Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
|
@ -183,7 +183,7 @@ jobs:
|
|||
# `github.event_name` would still be "push", not "workflow_call".
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
# Dual-registry publish. metadata-action expands the same tag set
|
||||
# against every image ref listed here, and build-push-action pushes
|
||||
|
|
|
|||
12
.github/workflows/gitleaks.yml
vendored
12
.github/workflows/gitleaks.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
|
@ -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]
|
||||
|
|
|
|||
2
.github/workflows/scorecard.yml
vendored
2
.github/workflows/scorecard.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/trivy.yml
vendored
6
.github/workflows/trivy.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
2
.github/workflows/workflow-lint.yml
vendored
2
.github/workflows/workflow-lint.yml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
|||
continue-on-error: true
|
||||
|
||||
- name: Upload SARIF
|
||||
uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
sarif_file: zizmor.sarif
|
||||
category: zizmor
|
||||
|
|
|
|||
12
.gitleaks.toml
Normal file
12
.gitleaks.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
title = "GitNexus"
|
||||
|
||||
[extend]
|
||||
useDefault = true
|
||||
|
||||
# Fake embedding API keys in unit tests (current probe + historical placeholder).
|
||||
[allowlist]
|
||||
description = "fake embedding API keys in http-embedder unit tests"
|
||||
regexes = [
|
||||
'''secret-key-12345''',
|
||||
'''test-api-key-redaction-check''',
|
||||
]
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
120
ARCHITECTURE.md
120
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<MyPhaseOutput> = {
|
|||
|
||||
---
|
||||
|
||||
## Call-Resolution DAG
|
||||
## Semantic model
|
||||
|
||||
Typed 6-stage pipeline in `call-processor.ts` (inside the `parse` phase) that resolves method/function calls and emits CALLS edges. Language behavior plugs in at two `LanguageProvider` hook points (stages 3–4); shared code names no languages. Scope: call resolution only — import resolution, type extraction, heritage, and symbol-table population live in other phases.
|
||||
`SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). The scope-resolution pipeline reads from here: `findOwnedMember`, `pickOverload`, and `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`.
|
||||
|
||||
### Stages
|
||||
|
||||
```
|
||||
extract-call ──▶ classify-form ──▶ infer-receiver ──▶ select-dispatch ──▶ resolve-target ──▶ emit-edge
|
||||
(1) (2) (3) [hook] (4) [hook] (5) (6)
|
||||
```
|
||||
|
||||
| Stage | Produces | Location |
|
||||
|-------|----------|----------|
|
||||
| **extract-call** | `ExtractedCallSite` (name, form, receiver, argCount) | `call-extractors/` (per-language); runs in worker |
|
||||
| **classify-form** | callForm (`free`/`member`/`constructor`) + arity | `call-analysis.ts` → `inferCallForm`; shared, runs in worker |
|
||||
| **infer-receiver** | `ReceiverEnriched` (receiver type finalized) | `call-processor.ts`; shared default chain, then `inferImplicitReceiver` hook |
|
||||
| **select-dispatch** | `DispatchDecision` (primary, fallback, ancestryView) | `selectDispatch` hook, falls back to shared default |
|
||||
| **resolve-target** | `TieredCandidates` | `model/resolve.ts` → `lookupMethodByOwnerWithMRO` (MRO walk) |
|
||||
| **emit-edge** | CALLS edge in graph | `call-processor.ts`; writes edge with confidence tier |
|
||||
|
||||
### Provider hooks
|
||||
|
||||
Both hooks are optional on `LanguageProvider`. Ruby is the only current implementer.
|
||||
|
||||
**`inferImplicitReceiver`** — called after shared infer-receiver defaults. Returns `ImplicitReceiverOverride | null`.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Inputs | `calledName`, `callForm`, `receiverName`, `receiverTypeName`, `callNode` (AST), `filePath` |
|
||||
| Non-null fields | `callForm`, `receiverName`, `receiverTypeName` (required); `receiverSource: 'implicit-self'` (fixed); `hint?` (opaque, passed to `selectDispatch`) |
|
||||
| Null | Keep existing `ReceiverEnriched` state |
|
||||
|
||||
**`selectDispatch`** — called after infer-receiver (including hook). Returns `DispatchDecision | null`; null uses shared default (constructor → `primary:'constructor'`; typed receiver → `primary:'owner-scoped'`; else → `primary:'free'`).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Inputs | `calledName`, `callForm`, `receiverName`, `receiverTypeName`, `receiverSource`, `hint` |
|
||||
| Non-null fields | `primary: 'owner-scoped' \| 'free' \| 'constructor'`; `fallback?: 'free-arity-narrowed'`; `ancestryView?: 'instance' \| 'singleton'`; `hint?` |
|
||||
|
||||
**`DispatchDecision` field semantics:**
|
||||
- `primary: 'owner-scoped'` — MRO walk from receiver's type; used when receiver type is known.
|
||||
- `fallback: 'free-arity-narrowed'` — after owner-scoped miss, search free-call candidates by arity only (Ruby uses this for implicit-self calls that miss their owner's MRO).
|
||||
- `ancestryView: 'singleton'` — walk singleton/class ancestry instead of instance ancestry (Ruby `def self.foo` bodies, so `extend`-ed methods are found).
|
||||
|
||||
### Adding language behavior
|
||||
|
||||
1. **Implicit receivers** — implement `inferImplicitReceiver`: return null if call already has a receiver; otherwise use `findEnclosingClassInfo` (`ast-helpers.ts`) to find the enclosing context, return `ImplicitReceiverOverride` with `receiverSource: 'implicit-self'`, and optionally set `hint` for `selectDispatch`.
|
||||
2. **Custom dispatch** — implement `selectDispatch`: inspect `receiverSource` and `hint`, return `DispatchDecision` with `primary`, optional `fallback`, optional `ancestryView`; return null to keep shared defaults.
|
||||
3. **MRO strategy** — confirm `mroStrategy` is `'first-wins'`, `'c3'`, `'ruby-mixin'`, or `'none'`; consumed by `lookupMethodByOwnerWithMRO`.
|
||||
|
||||
**Ruby example** (`languages/ruby.ts` + `utils/ruby-self-call.ts`): `inferImplicitReceiver` rewrites bare-identifier calls to `self.method` and sets `hint` to `'instance'`/`'singleton'`; `selectDispatch` uses hint for `ancestryView` and adds `fallback: 'free-arity-narrowed'` for implicit-self calls.
|
||||
|
||||
### Code references
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `core/ingestion/call-types.ts` | DAG types: `ReceiverEnriched`, `DispatchDecision`, `ImplicitReceiverOverride` |
|
||||
| `core/ingestion/language-provider.ts` | Hook signatures: `inferImplicitReceiver`, `selectDispatch` |
|
||||
| `core/ingestion/call-processor.ts` | `processCalls`: stages 3–6 |
|
||||
| `core/ingestion/model/resolve.ts` | `lookupMethodByOwnerWithMRO`: stage 5 MRO walk |
|
||||
| `core/ingestion/languages/ruby.ts` | Both hooks + `mroStrategy: 'ruby-mixin'` |
|
||||
| `core/ingestion/utils/ruby-self-call.ts` | Bare-call rewrite for `inferImplicitReceiver` |
|
||||
|
||||
### Coexistence with the scope-resolution pipeline
|
||||
|
||||
The Call-Resolution DAG is the **legacy path**. RFC #909 Ring 3 introduces a parallel **scope-resolution pipeline** (next section) that replaces stages 1–6 with a scope-indexed registry lookup. Both paths ship side-by-side and are gated per-language via `MIGRATED_LANGUAGES` + the `REGISTRY_PRIMARY_<LANG>` env var.
|
||||
|
||||
- **Unmigrated language** → Call-Resolution DAG runs; scope-resolution phase is a no-op.
|
||||
- **Migrated language** (currently: Python, C#) → scope-resolution owns CALLS/ACCESSES/USES emission; the legacy DAG gates off for that language via `isRegistryPrimary(lang)` checks in `call-processor.ts` and `import-processor.ts`.
|
||||
- `import-processor` still populates `importMap` for migrated languages — heritage's `ctx.resolve` reads it to disambiguate parent classes. Only edge emission is gated.
|
||||
- CI runs BOTH paths for every migrated language on every PR (`.github/workflows/ci-scope-parity.yml`); both must pass.
|
||||
|
||||
#### Same-graph guarantee
|
||||
|
||||
Edges emitted by the scope-resolution pipeline and edges emitted by the legacy DAG are indistinguishable to downstream consumers (MCP tools, HTTP API, embeddings, group bridge):
|
||||
|
||||
- **Node identity** — both paths use `generateId(...)` from `lib/utils.ts`, the same qualified-name keyspace, and the same node labels (`File`, `Folder`, `Class`, `Method`, `Function`, …). Overload disambiguation suffixes `parameterTypes` into the id consistently — see `scope-resolution/graph-bridge/ids.ts` and the legacy emitter in `call-processor.ts`.
|
||||
- **Edge vocabulary** — both paths emit the same reasons: `'import-resolved' | 'global' | 'local-call' | 'same-file' | 'interface-dispatch' | 'read' | 'write'`. Migrating a language must not change which reasons consumers see for previously-resolved edges.
|
||||
- **Confidence tier** — both paths attach a numeric `confidence` to each edge using the same scale.
|
||||
|
||||
The CI parity workflow (`.github/workflows/ci-scope-parity.yml`) runs both paths against every migrated language's fixture corpus and fails on any divergence.
|
||||
|
||||
#### Semantic-model source of truth
|
||||
|
||||
Two independent invariants.
|
||||
|
||||
**ParsedFile = the AST-level truth.** `ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact both resolution paths consume. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
|
||||
|
||||
**SemanticModel = the symbol-level truth.** `SemanticModel` (`gitnexus/src/core/ingestion/model/semantic-model.ts`) is the authoritative store for every symbol-indexed lookup (by `nodeId`, `simpleName`, `qualifiedName`, or `filePath`). Both paths read from here:
|
||||
|
||||
- Legacy Call-Resolution DAG → `call-processor` Tier 1/2/3 via `model.symbols.lookupExactAll`, `model.methods.lookupMethodByName`, `model.types.lookupClassByName`, `lookupMethodByOwnerWithMRO`.
|
||||
- Scope-resolution pipeline → `findOwnedMember`, `pickOverload`, `findExportedDefByName` all consult `model.methods` / `model.fields` / `model.symbols`.
|
||||
`ParsedFile` (`gitnexus-shared/src/scope-resolution/parsed-file.ts`) is the single per-file artifact the scope-resolution pipeline consumes. Scope-resolution passes MUST NOT build a parallel parse representation. If a per-language hook needs AST-level facts that `ParsedFile` doesn't expose, it should reuse the orchestrator's `treeCache` (`RunScopeResolutionInput.treeCache`) rather than re-invoking `parser.parse(...)` on its own — the C# `populateNamespaceSiblings` hook is the reference implementation of this pattern.
|
||||
|
||||
The scope-resolution pipeline additionally carries `WorkspaceResolutionIndex` for `Scope`-valued lookups (`classScopeByDefId`, `moduleScopeByFile`) that `SemanticModel` structurally cannot hold. No symbol-indexed duplicates exist outside `SemanticModel`.
|
||||
|
||||
**Write / read phase contract.** The model is mutable during three ordered phases and read-only afterward:
|
||||
|
||||
```
|
||||
Phase 1: legacy parse ──► symbolTable.add fans into types/methods/fields
|
||||
Phase 1: parse ──► symbolTable.add fans into types/methods/fields
|
||||
Phase 2: scope-resolution ──► reconcileOwnership() registers corrected ownerIds
|
||||
Phase 3: finalize ──► model.attachScopeIndexes(bundle) — one-shot freeze
|
||||
─────────────────────────── phase boundary ───────────────────────────
|
||||
|
|
@ -255,7 +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/<lang>/scope-resolver.ts`.
|
||||
2. Add entry to `SCOPE_RESOLVERS` in `scope-resolution/pipeline/registry.ts`.
|
||||
3. Add the language to `MIGRATED_LANGUAGES` in `registry-primary-flag.ts` when the shadow-harness corpus parity ≥ 99% fixtures / ≥ 98% corpus.
|
||||
|
||||
CI auto-discovers the set via `tsx`. No workflow edit required.
|
||||
|
||||
|
|
@ -328,7 +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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
35
README.md
35
README.md
|
|
@ -22,6 +22,9 @@
|
|||
<a href="https://securityscorecards.dev/viewer/?uri=github.com/abhigyanpatwari/GitNexus">
|
||||
<img src="https://api.securityscorecards.dev/projects/github.com/abhigyanpatwari/GitNexus/badge" alt="OpenSSF Scorecard"/>
|
||||
</a>
|
||||
<a href="https://github.com/abhigyanpatwari/GitNexus/actions/workflows/ci.yml">
|
||||
<img src="https://github.com/abhigyanpatwari/GitNexus/actions/workflows/ci.yml/badge.svg" alt="CI Workflows"/>
|
||||
</a>
|
||||
|
||||
<p><strong>Enterprise (SaaS & Self-hosted)</strong> - <a href="https://akonlabs.com">akonlabs.com</a></p>
|
||||
|
||||
|
|
@ -226,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 <n>` to choose a higher limit while still keeping memory bounded.
|
||||
|
||||
#### Project config (`.gitnexusrc`)
|
||||
|
||||
Commit a `.gitnexusrc` JSON file at the repo root to preconfigure recurring `analyze` options per project, instead of re-passing the same flags every run. It is read from the resolved repo root (not `.gitnexus/`, which is gitignored index storage). **CLI flags always override `.gitnexusrc`.**
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Default branch used in the generated regression-compare example (base_ref).
|
||||
// Use this so a project on `develop`/`master` doesn't get "main" rewritten
|
||||
// over its fix on every analyze. (Alias: "branch".)
|
||||
"defaultBranch": "develop",
|
||||
"skipContextFiles": true, // alias of skipAgentsMd: keep your own AGENTS.md/CLAUDE.md
|
||||
"skipSkills": true, // don't install .claude/skills/gitnexus/
|
||||
"embeddings": true, // generate embeddings by default
|
||||
"workerTimeout": 60
|
||||
}
|
||||
```
|
||||
|
||||
A nested `analyze` block is also accepted (and overrides flat keys for the same option):
|
||||
|
||||
```json
|
||||
{ "analyze": { "defaultBranch": "develop", "skipSkills": true } }
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The default branch is resolved as: `--default-branch` > `.gitnexusrc` `defaultBranch`/`branch` > auto-detected `origin/HEAD` > `main`.
|
||||
- `skipContextFiles` / `skipAiContext` are aliases for `skipAgentsMd` — they skip the `AGENTS.md` / `CLAUDE.md` block only. They do **not** imply `skipSkills`. `indexOnly` is the stronger option that skips all file injection.
|
||||
- Supported keys: `defaultBranch` (`branch`), `skipAgentsMd` (`skipContextFiles`, `skipAiContext`), `skipSkills`, `indexOnly`, `stats`/`noStats`, `embeddings`, `dropEmbeddings`, `name`, `allowDuplicateName`, `maxFileSize`, `workerTimeout`, `walCheckpointThreshold`, `workers`, `embeddingThreads`, `embeddingBatchSize`, `embeddingSubBatchSize`, `embeddingDevice`.
|
||||
- The file is JSON only. Unknown keys and invalid values fail fast with an actionable error before analysis starts.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
Most `analyze` knobs are also CLI flags (`--workers`, `--worker-timeout`, `--max-file-size`, `--verbose`). Use the env-var form when you'd otherwise repeat the same flag every run, or when invoking GitNexus from a long-running host (MCP server, eval-server, CI shell) that already manages its own environment. CLI flags take precedence over env vars; env vars take precedence over built-in defaults.
|
||||
|
|
|
|||
74
TESTING.md
74
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_<LANG>` flag were removed in RING4-1 (#942). Each language's resolver test lives at `test/integration/resolvers/<slug>.test.ts` and runs once, on the single scope-resolution path, as part of the normal `tests` job (`vitest test/**/*.test.ts`).
|
||||
|
||||
For each migrated language, CI runs the resolver test file twice:
|
||||
1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path
|
||||
2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path
|
||||
|
||||
Both must pass. Known legacy gaps are listed in `LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES` in `test/integration/resolvers/helpers.ts` and are automatically skipped in legacy mode.
|
||||
|
||||
Adding a language to `MIGRATED_LANGUAGES` automatically enrolls it in parity — no workflow or config edit needed. The test file must exist at `test/integration/resolvers/<slug>.test.ts`.
|
||||
|
||||
Run parity locally: `cd gitnexus && npm run test:parity`
|
||||
|
||||
Run for a single language: `cd gitnexus && npx tsx scripts/run-parity.ts --language python`
|
||||
Adding a language: register its `ScopeResolver` in `scope-resolution/pipeline/registry.ts` (`SCOPE_RESOLVERS`) and add the resolver test file — no workflow or config edit needed.
|
||||
|
||||
## Cross-platform testing
|
||||
|
||||
|
|
@ -120,12 +110,12 @@ To check the cross-platform list is up to date, run `npm run test:cross-platform
|
|||
|
||||
GitHub Actions (`.github/workflows/ci.yml`) orchestrate:
|
||||
|
||||
| Workflow | Jobs | Purpose |
|
||||
| --------------------- | ------------------------------ | ------------------------------------------------ |
|
||||
| `ci-quality.yml` | format, lint, typecheck, typecheck-web, workflow-convention | Code quality gates |
|
||||
| Workflow | Jobs | Purpose |
|
||||
| --------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `ci-quality.yml` | format, lint, typecheck, typecheck-web, workflow-convention | Code quality gates |
|
||||
| `ci-tests.yml` | ubuntu/coverage, cross-platform (Win/Mac), packaged-install-smoke | Full suite + coverage on Ubuntu; platform-sensitive subset on Win/Mac |
|
||||
| `ci-scope-parity.yml` | discover, parity | Scope-resolution parity for all migrated languages |
|
||||
| `ci-e2e.yml` | e2e (chromium) | Playwright E2E, gated on `gitnexus-web/**` changes |
|
||||
| `ci-scope-parity.yml` | discover, parity | Scope-resolution parity for all migrated languages |
|
||||
| `ci-e2e.yml` | e2e (chromium) | Playwright E2E, gated on `gitnexus-web/**` changes |
|
||||
|
||||
The `CI Gate` job in `ci.yml` is the single required check for branch protection. It requires quality, tests, e2e, and scope-parity to all pass.
|
||||
|
||||
|
|
|
|||
196
eval/uv.lock
generated
196
eval/uv.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
134
gitnexus-web/package-lock.json
generated
134
gitnexus-web/package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
06687dff942d531c4d453b5906a8666c90db4867eb43ed18304aa59a8a93ef9d
|
||||
c03f87cd8cd1ee716dea93cceb27109ee5b4584bc9fe426eea3f18fd6f9854cc
|
||||
|
|
|
|||
|
|
@ -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<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.)"
|
||||
"_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.) | #942: scope-resolution-only cleanup reworded fixture comments; capture byte-positions shift, capture LOGIC unchanged."
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
gitnexus/package-lock.json
generated
6
gitnexus/package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@
|
|||
* isolating the resolution cost from parse / heritage / pipeline
|
||||
* overhead.
|
||||
*
|
||||
* Usage: REGISTRY_PRIMARY_PYTHON=1 npx tsx scripts/bench-scope-resolution.ts
|
||||
* Usage: npx tsx scripts/bench-scope-resolution.ts
|
||||
*/
|
||||
process.env.REGISTRY_PRIMARY_PYTHON = '1';
|
||||
|
||||
import { generateId } from '../src/lib/utils.js';
|
||||
import { createKnowledgeGraph } from '../src/core/graph/graph.js';
|
||||
import { runScopeResolution } from '../src/core/ingestion/scope-resolution/index.js';
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
/**
|
||||
* CI helper — emits the `MIGRATED_LANGUAGES` set as a JSON matrix array for
|
||||
* GitHub Actions (`.github/workflows/ci-scope-parity.yml`).
|
||||
*
|
||||
* Consumed by the `discover` job in that workflow. Each entry has:
|
||||
* - `slug`: lowercase language id, matching `test/integration/resolvers/<slug>.test.ts`.
|
||||
* - `envvar`: uppercase suffix used to build the `REGISTRY_PRIMARY_<envvar>` toggle.
|
||||
*
|
||||
* Run with `npx tsx scripts/ci-list-migrated-languages.ts`. The script
|
||||
* writes a single JSON array to stdout (no wrapper object) so the
|
||||
* workflow can pipe it straight into `$GITHUB_OUTPUT`.
|
||||
*/
|
||||
|
||||
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
||||
|
||||
const entries = [...MIGRATED_LANGUAGES].map((slug) => {
|
||||
const s = String(slug);
|
||||
return {
|
||||
slug: s,
|
||||
envvar: s.toUpperCase().replace(/-/g, '_'),
|
||||
};
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify(entries));
|
||||
|
|
@ -1,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_<LANG>=0 (legacy DAG)
|
||||
* 2. Run its resolver test with REGISTRY_PRIMARY_<LANG>=1 (registry-primary)
|
||||
*
|
||||
* Both modes must pass. Failures are collected and reported at the end
|
||||
* so all regressions are visible in a single CI run (equivalent to the
|
||||
* old workflow's fail-fast: false behavior).
|
||||
*
|
||||
* Vitest output streams to the console in real time (stdio: 'inherit')
|
||||
* so CI logs show the actual test output directly. No per-invocation
|
||||
* timeout — the CI job-level timeout (30 min) is the outer guard.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/run-parity.ts
|
||||
* npx tsx scripts/run-parity.ts --language python # single language
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { MIGRATED_LANGUAGES } from '../src/core/ingestion/registry-primary-flag.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
interface ParityFailure {
|
||||
lang: string;
|
||||
mode: 'legacy' | 'registry-primary';
|
||||
}
|
||||
|
||||
function envVarName(slug: string): string {
|
||||
return `REGISTRY_PRIMARY_${slug.toUpperCase().replace(/-/g, '_')}`;
|
||||
}
|
||||
|
||||
function 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<string, string>): boolean {
|
||||
try {
|
||||
execFileSync('npx', ['vitest', 'run', testFile], {
|
||||
cwd: ROOT,
|
||||
env: { ...process.env, ...env },
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CLI args
|
||||
const args = process.argv.slice(2);
|
||||
const langFlag = args.indexOf('--language');
|
||||
const singleLang = langFlag >= 0 ? args[langFlag + 1] : undefined;
|
||||
|
||||
if (langFlag >= 0 && singleLang === undefined) {
|
||||
console.error('--language requires a value');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const languages = singleLang ? [singleLang] : [...MIGRATED_LANGUAGES].map(String);
|
||||
|
||||
// Verify test files exist before running
|
||||
const missingFiles: string[] = [];
|
||||
const filesByLanguage = new Map<string, string[]>();
|
||||
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.');
|
||||
|
|
@ -29,6 +29,12 @@ export interface AIContextOptions {
|
|||
skipAgentsMd?: boolean;
|
||||
noStats?: boolean;
|
||||
skipSkills?: boolean;
|
||||
/**
|
||||
* Default branch used by the generated regression-compare example (#243).
|
||||
* Resolved by the CLI (CLI flag > `.gitnexusrc` > auto-detect > "main"); a
|
||||
* plain caller that omits it gets "main", preserving prior behavior.
|
||||
*/
|
||||
defaultBranch?: string;
|
||||
}
|
||||
|
||||
const GITNEXUS_START_MARKER = '<!-- gitnexus:start -->';
|
||||
|
|
@ -89,6 +95,16 @@ async function findGroupsContainingRegistryName(registryName: string): Promise<s
|
|||
return hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip backticks from a branch name before it is embedded in a Markdown
|
||||
* inline-code span (#1996 tri-review P1). validateBranchName already rejects
|
||||
* backticks for CLI/config/auto-detect inputs; this is the last-line defense at
|
||||
* the generation sink so the embedding is provably safe regardless of caller.
|
||||
*/
|
||||
export function markdownSafeBranch(branch: string): string {
|
||||
return branch.replace(/`/g, '');
|
||||
}
|
||||
|
||||
export function generateGitNexusContent(
|
||||
projectName: string,
|
||||
stats: RepoStats,
|
||||
|
|
@ -100,6 +116,14 @@ export function generateGitNexusContent(
|
|||
// index (#1945). Referenced by docs so a single CLI-neutral command resolves
|
||||
// the available runner (global `gitnexus` → `pnpm dlx` → `npx`) at call time.
|
||||
runnerPath: string = '.gitnexus/run.cjs',
|
||||
// Default branch for the regression-compare example (#243). Configurable so
|
||||
// projects on `develop`/`master`/etc. don't get `base_ref: "main"` rewritten
|
||||
// back over their fix on every analyze. The value is embedded inside a
|
||||
// Markdown inline-code span: validateBranchName rejects backticks upstream,
|
||||
// and `markdownSafeBranch` strips any remaining backtick here as defense in
|
||||
// depth, so JSON.stringify's quote/escape handling is sufficient and the
|
||||
// branch cannot break out of the span (#1996 tri-review P1).
|
||||
defaultBranch: string = 'main',
|
||||
): string {
|
||||
const generatedRows =
|
||||
generatedSkills && generatedSkills.length > 0
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
426
gitnexus/src/cli/analyze-config.ts
Normal file
426
gitnexus/src/cli/analyze-config.ts
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
/**
|
||||
* Project-local `.gitnexusrc` support for `gitnexus analyze` (#243).
|
||||
*
|
||||
* Lets a repository commit recurring `analyze` defaults — default branch for the
|
||||
* generated regression example, AI-context / skills opt-outs, embedding knobs —
|
||||
* so contributors don't re-pass the same flags on every run. Design rules:
|
||||
*
|
||||
* - Config is repo-local (`.gitnexusrc` at the resolved repo root). It is NOT
|
||||
* read from `.gitnexus/` because that directory is index storage and is
|
||||
* commonly gitignored.
|
||||
* - JSON only. No YAML, no `package.json` field, no global `~/.gitnexus` file
|
||||
* in this pass.
|
||||
* - CLI flags always override config (see {@link mergeAnalyzeOptions}).
|
||||
* - Fail closed: unknown keys, wrong value types, conflicting aliases, and
|
||||
* invalid JSON all throw {@link GitNexusRcError} so a typo never silently
|
||||
* no-ops. Errors are actionable and surface before any expensive analysis.
|
||||
* - Config values never reach a shell. Strings are validated against control
|
||||
* and hidden/bidirectional characters so they cannot inject markdown,
|
||||
* JSON, or hidden controls into generated context files.
|
||||
*
|
||||
* Both a flat shape and a nested `analyze` block are accepted:
|
||||
*
|
||||
* { "defaultBranch": "develop", "skipContextFiles": true }
|
||||
* { "analyze": { "defaultBranch": "develop", "skipSkills": true } }
|
||||
*
|
||||
* When both set the same option, the nested `analyze` block wins (deterministic,
|
||||
* documented precedence). Conflicting *aliases at the same level* (e.g. both
|
||||
* `defaultBranch` and `branch`) are rejected.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { AnalyzeOptions } from './analyze.js';
|
||||
|
||||
export const GITNEXUS_RC_FILENAME = '.gitnexusrc';
|
||||
|
||||
/** Final fallback when no branch is configured or detectable. */
|
||||
export const DEFAULT_BRANCH_FALLBACK = 'main';
|
||||
|
||||
/** Git refs longer than this are almost certainly a mistake / injection attempt. */
|
||||
const BRANCH_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Thrown for any `.gitnexusrc` problem (missing-file is NOT an error — it
|
||||
* returns `undefined`). The message is user-facing and names the file so the
|
||||
* CLI can print it verbatim before starting the progress bar.
|
||||
*/
|
||||
export class GitNexusRcError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'GitNexusRcError';
|
||||
}
|
||||
}
|
||||
|
||||
type ValueKind =
|
||||
| 'boolean'
|
||||
| 'boolean-negate'
|
||||
| 'string'
|
||||
| 'numeric-string'
|
||||
| 'embeddings'
|
||||
| 'branch';
|
||||
|
||||
interface KeySpec {
|
||||
/** The `AnalyzeOptions` field this config key normalizes into. */
|
||||
target: keyof AnalyzeOptions;
|
||||
kind: ValueKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowed `.gitnexusrc` keys and how each maps onto `AnalyzeOptions`.
|
||||
*
|
||||
* Aliases intentionally collapse onto a shared target:
|
||||
* - `branch` is the legacy alias for `defaultBranch` (issue-comment shape).
|
||||
* - `skipContextFiles` / `skipAiContext` are aliases for `skipAgentsMd` — they
|
||||
* suppress the AGENTS.md / CLAUDE.md block ONLY. They do not imply
|
||||
* `skipSkills`, and they are weaker than `indexOnly` (which skips all
|
||||
* file injection). This matches the existing CLI semantics exactly.
|
||||
* - `noStats` is the negation of `stats`.
|
||||
*/
|
||||
const KEY_SPECS: Record<string, KeySpec> = {
|
||||
defaultBranch: { target: 'defaultBranch', kind: 'branch' },
|
||||
branch: { target: 'defaultBranch', kind: 'branch' },
|
||||
skipAgentsMd: { target: 'skipAgentsMd', kind: 'boolean' },
|
||||
skipContextFiles: { target: 'skipAgentsMd', kind: 'boolean' },
|
||||
skipAiContext: { target: 'skipAgentsMd', kind: 'boolean' },
|
||||
skipSkills: { target: 'skipSkills', kind: 'boolean' },
|
||||
indexOnly: { target: 'indexOnly', kind: 'boolean' },
|
||||
stats: { target: 'stats', kind: 'boolean' },
|
||||
noStats: { target: 'stats', kind: 'boolean-negate' },
|
||||
embeddings: { target: 'embeddings', kind: 'embeddings' },
|
||||
dropEmbeddings: { target: 'dropEmbeddings', kind: 'boolean' },
|
||||
name: { target: 'name', kind: 'string' },
|
||||
allowDuplicateName: { target: 'allowDuplicateName', kind: 'boolean' },
|
||||
maxFileSize: { target: 'maxFileSize', kind: 'numeric-string' },
|
||||
workerTimeout: { target: 'workerTimeout', kind: 'numeric-string' },
|
||||
walCheckpointThreshold: { target: 'walCheckpointThreshold', kind: 'numeric-string' },
|
||||
workers: { target: 'workers', kind: 'numeric-string' },
|
||||
embeddingThreads: { target: 'embeddingThreads', kind: 'numeric-string' },
|
||||
embeddingBatchSize: { target: 'embeddingBatchSize', kind: 'numeric-string' },
|
||||
embeddingSubBatchSize: { target: 'embeddingSubBatchSize', kind: 'numeric-string' },
|
||||
embeddingDevice: { target: 'embeddingDevice', kind: 'string' },
|
||||
};
|
||||
|
||||
/** Top-level container key for the nested form; not itself an `AnalyzeOptions` field. */
|
||||
const NESTED_KEY = 'analyze';
|
||||
|
||||
const ALLOWED_KEYS_HINT = `Allowed keys: ${Object.keys(KEY_SPECS).join(', ')} (or a nested "${NESTED_KEY}" object).`;
|
||||
|
||||
/**
|
||||
* Reject control characters and hidden / bidirectional Unicode in a string
|
||||
* value. These have no legitimate place in a branch name, registry name, or
|
||||
* device string, and would otherwise let a committed config smuggle invisible
|
||||
* controls into generated AGENTS.md / CLAUDE.md content.
|
||||
*/
|
||||
const isHiddenOrControl = (codePoint: number): boolean =>
|
||||
codePoint < 0x20 ||
|
||||
codePoint === 0x7f ||
|
||||
(codePoint >= 0x200b && codePoint <= 0x200f) || // zero-width + LRM/RLM
|
||||
(codePoint >= 0x202a && codePoint <= 0x202e) || // bidi embeddings/overrides
|
||||
(codePoint >= 0x2060 && codePoint <= 0x2064) || // word-joiner + invisible math
|
||||
(codePoint >= 0x2066 && codePoint <= 0x206f) || // bidi isolates + deprecated
|
||||
codePoint === 0xfeff; // BOM / zero-width no-break space
|
||||
|
||||
const assertNoHiddenChars = (value: string, source: string): void => {
|
||||
for (const ch of value) {
|
||||
const cp = ch.codePointAt(0);
|
||||
if (cp !== undefined && isHiddenOrControl(cp)) {
|
||||
throw new GitNexusRcError(
|
||||
`${source}: value contains control or hidden/bidirectional characters, which are not allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a user-supplied branch name (from CLI or `.gitnexusrc`). Returns the
|
||||
* trimmed name or throws {@link GitNexusRcError}. Conservative but accepts the
|
||||
* shapes real branches use (`feature/foo-bar`, `release/1.2`, `develop`).
|
||||
*/
|
||||
export function validateBranchName(value: string, source: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new GitNexusRcError(`${source}: branch name must not be empty.`);
|
||||
}
|
||||
if (trimmed.length > BRANCH_MAX_LENGTH) {
|
||||
throw new GitNexusRcError(`${source}: branch name is too long (max ${BRANCH_MAX_LENGTH}).`);
|
||||
}
|
||||
assertNoHiddenChars(trimmed, source);
|
||||
if (/\s/.test(trimmed)) {
|
||||
throw new GitNexusRcError(`${source}: branch name must not contain whitespace.`);
|
||||
}
|
||||
// git ref-name rules (subset): reject characters git itself forbids in refs.
|
||||
if (/[~^:?*[\\]/.test(trimmed)) {
|
||||
throw new GitNexusRcError(
|
||||
`${source}: branch name contains characters not allowed in a git ref (~ ^ : ? * [ \\).`,
|
||||
);
|
||||
}
|
||||
if (trimmed.startsWith('-')) {
|
||||
throw new GitNexusRcError(`${source}: branch name must not start with "-".`);
|
||||
}
|
||||
if (trimmed.includes('..')) {
|
||||
throw new GitNexusRcError(`${source}: branch name must not contain "..".`);
|
||||
}
|
||||
// Git permits a backtick in a ref, but the branch is embedded inside a
|
||||
// Markdown inline-code span in the generated AGENTS.md/CLAUDE.md regression
|
||||
// example, where a backtick would close the span early and let the rest of
|
||||
// the template render as instruction text. Reject it at this single
|
||||
// chokepoint so all three tiers (CLI flag, .gitnexusrc, auto-detect via
|
||||
// sanitizeDetectedBranch) are covered (#1996 tri-review P1).
|
||||
if (trimmed.includes('`')) {
|
||||
throw new GitNexusRcError(
|
||||
`${source}: branch name must not contain a backtick (it would break the generated Markdown).`,
|
||||
);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort validation for an auto-detected branch (from git). Never throws —
|
||||
* returns `undefined` for anything unusable so the resolver falls back to the
|
||||
* next precedence tier.
|
||||
*/
|
||||
export function sanitizeDetectedBranch(value: string | null | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
try {
|
||||
return validateBranchName(value, 'detected branch');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeValue = (kind: ValueKind, value: unknown, key: string): unknown => {
|
||||
const source = `${GITNEXUS_RC_FILENAME} "${key}"`;
|
||||
switch (kind) {
|
||||
case 'boolean':
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new GitNexusRcError(`${source} must be a boolean (true/false).`);
|
||||
}
|
||||
return value;
|
||||
case 'boolean-negate':
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new GitNexusRcError(`${source} must be a boolean (true/false).`);
|
||||
}
|
||||
return !value;
|
||||
case 'branch':
|
||||
if (typeof value !== 'string') {
|
||||
throw new GitNexusRcError(`${source} must be a string branch name.`);
|
||||
}
|
||||
return validateBranchName(value, source);
|
||||
case 'string': {
|
||||
if (typeof value !== 'string') {
|
||||
throw new GitNexusRcError(`${source} must be a string.`);
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new GitNexusRcError(`${source} must not be empty.`);
|
||||
}
|
||||
assertNoHiddenChars(trimmed, source);
|
||||
// `name` flows into the generated AGENTS.md/CLAUDE.md as `**${name}**` and
|
||||
// inside `gitnexus://repo/${name}/…` code spans, so a Markdown-significant
|
||||
// character would break those spans or inject emphasis/links/HTML into
|
||||
// agent-instruction content (#1996 tri-review P1). `_` is intentionally
|
||||
// allowed (legitimate in repo names; intraword `_` is not emphasis).
|
||||
// embeddingDevice (the other `string`-kind option) only ever holds a
|
||||
// fixed device token, so this guard never rejects a valid value there.
|
||||
if (/[`*[\]<>]/.test(trimmed)) {
|
||||
throw new GitNexusRcError(
|
||||
`${source} must not contain Markdown-significant characters (\` * [ ] < >).`,
|
||||
);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
case 'numeric-string': {
|
||||
// Mirror Commander's contract: these options reach the existing CLI
|
||||
// validation as strings. Accept a JSON number or a string; normalize to a
|
||||
// string and let the downstream per-flag validation enforce ranges so the
|
||||
// error messages stay in one place.
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new GitNexusRcError(`${source} must be a finite number.`);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new GitNexusRcError(`${source} must not be empty.`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
throw new GitNexusRcError(`${source} must be a number or numeric string.`);
|
||||
}
|
||||
case 'embeddings': {
|
||||
// Mirror `--embeddings [limit]`: boolean toggles, a non-negative integer
|
||||
// sets the node cap (normalized to a string, as Commander would supply).
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
throw new GitNexusRcError(
|
||||
`${source} must be true/false or a non-negative integer (node cap; 0 disables the cap).`,
|
||||
);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
throw new GitNexusRcError(
|
||||
`${source} must be a boolean or a non-negative integer (node cap; 0 disables the cap).`,
|
||||
);
|
||||
}
|
||||
default:
|
||||
// Exhaustive — kept for forward-compat if a new kind is added.
|
||||
throw new GitNexusRcError(`${source}: unsupported config value kind.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize one level (flat top-level or the nested `analyze` block) into a
|
||||
* partial `AnalyzeOptions`. Rejects unknown keys and two aliases that configure
|
||||
* the same option at the same level.
|
||||
*/
|
||||
const normalizeLevel = (
|
||||
obj: Record<string, unknown>,
|
||||
{ allowNestedKey }: { allowNestedKey: boolean },
|
||||
): Partial<AnalyzeOptions> => {
|
||||
const out: Partial<AnalyzeOptions> = {};
|
||||
const setBy = new Map<keyof AnalyzeOptions, string>();
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (allowNestedKey && key === NESTED_KEY) continue; // handled separately
|
||||
// `Object.hasOwn`, not a truthiness check: a plain-object lookup like
|
||||
// `KEY_SPECS["__proto__"]` returns an inherited member (Object.prototype,
|
||||
// truthy) and would slip past `if (!spec)`, hitting the wrong error branch
|
||||
// instead of the documented "Unknown key" message (#1996 tri-review P3).
|
||||
if (!Object.hasOwn(KEY_SPECS, key)) {
|
||||
throw new GitNexusRcError(
|
||||
`Unknown key "${key}" in ${GITNEXUS_RC_FILENAME}. ${ALLOWED_KEYS_HINT}`,
|
||||
);
|
||||
}
|
||||
const spec = KEY_SPECS[key];
|
||||
const prev = setBy.get(spec.target);
|
||||
if (prev && prev !== key) {
|
||||
throw new GitNexusRcError(
|
||||
`${GITNEXUS_RC_FILENAME}: "${prev}" and "${key}" both configure the same option; set only one.`,
|
||||
);
|
||||
}
|
||||
setBy.set(spec.target, key);
|
||||
(out as Record<string, unknown>)[spec.target] = normalizeValue(spec.kind, value, key);
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Locate, read, parse, validate, and normalize `.gitnexusrc` at `repoRoot`.
|
||||
*
|
||||
* @returns the normalized config defaults, or `undefined` when no file exists
|
||||
* (the normal case). Throws {@link GitNexusRcError} on any problem.
|
||||
*/
|
||||
export function loadAnalyzeConfig(repoRoot: string): Partial<AnalyzeOptions> | undefined {
|
||||
const filePath = path.join(repoRoot, GITNEXUS_RC_FILENAME);
|
||||
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, 'utf-8');
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined;
|
||||
throw new GitNexusRcError(`Could not read ${GITNEXUS_RC_FILENAME}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// Strip a leading UTF-8 BOM: Node's 'utf-8' decode keeps it, and JSON.parse
|
||||
// then fails with a confusing "Unexpected token" on an otherwise-valid file
|
||||
// (#1996 tri-review). Only one leading BOM is stripped; in-string control
|
||||
// rejection still applies to the parsed values.
|
||||
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
throw new GitNexusRcError(
|
||||
`${GITNEXUS_RC_FILENAME} is not valid JSON: ${(err as Error).message}. ` +
|
||||
`Expected a JSON object such as {"defaultBranch": "develop", "skipContextFiles": true}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new GitNexusRcError(`${GITNEXUS_RC_FILENAME} must contain a JSON object.`);
|
||||
}
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
const flat = normalizeLevel(obj, { allowNestedKey: true });
|
||||
|
||||
let nested: Partial<AnalyzeOptions> = {};
|
||||
if (Object.prototype.hasOwnProperty.call(obj, NESTED_KEY)) {
|
||||
const nestedRaw = obj[NESTED_KEY];
|
||||
if (nestedRaw === null || typeof nestedRaw !== 'object' || Array.isArray(nestedRaw)) {
|
||||
throw new GitNexusRcError(`${GITNEXUS_RC_FILENAME} "${NESTED_KEY}" must be a JSON object.`);
|
||||
}
|
||||
nested = normalizeLevel(nestedRaw as Record<string, unknown>, { allowNestedKey: false });
|
||||
}
|
||||
|
||||
// Nested `analyze` block overrides flat top-level keys for the same option.
|
||||
return { ...flat, ...nested };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge CLI options over `.gitnexusrc` defaults. CLI wins whenever it provides a
|
||||
* value — including an explicit `false` (so an explicit CLI off-switch beats a
|
||||
* config `true`). Config fills only the genuinely-unset (`undefined`) options.
|
||||
*
|
||||
* `stats` is special: Commander always materializes it (`true` by default,
|
||||
* `false` only when `--no-stats` is passed), so plain `??` can't tell "default
|
||||
* on" from "explicitly on". The rule is: a passed `--no-stats` (`stats: false`)
|
||||
* always wins; otherwise the config value applies. There is intentionally no
|
||||
* `--stats` counter-flag, so config can only turn stats off, not force it back
|
||||
* on against a `--no-stats`.
|
||||
*
|
||||
* `defaultBranch` is NOT resolved here — see {@link resolveDefaultBranch}, which
|
||||
* applies the CLI > config > auto-detect > "main" precedence chain.
|
||||
*/
|
||||
export function mergeAnalyzeOptions(
|
||||
cli: AnalyzeOptions,
|
||||
config: Partial<AnalyzeOptions> | undefined,
|
||||
): AnalyzeOptions {
|
||||
if (!config) return cli;
|
||||
|
||||
const merged: AnalyzeOptions = { ...cli };
|
||||
for (const key of Object.keys(config) as (keyof AnalyzeOptions)[]) {
|
||||
if (key === 'stats' || key === 'defaultBranch') continue; // handled below / by resolver
|
||||
if (merged[key] === undefined) {
|
||||
(merged as Record<string, unknown>)[key] = config[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (config.stats !== undefined && cli.stats !== false) {
|
||||
merged.stats = config.stats;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default branch threaded into generated context, applying the
|
||||
* precedence chain:
|
||||
*
|
||||
* CLI `--default-branch` > `.gitnexusrc` `defaultBranch`/`branch`
|
||||
* > auto-detected `origin/HEAD` > {@link DEFAULT_BRANCH_FALLBACK} ("main").
|
||||
*
|
||||
* User-supplied values (CLI, config) are validated strictly and throw on bad
|
||||
* input. The auto-detected value is best-effort and silently ignored if
|
||||
* unusable.
|
||||
*/
|
||||
export function resolveDefaultBranch(input: {
|
||||
cliBranch?: string;
|
||||
configBranch?: string;
|
||||
detectedBranch?: string | null;
|
||||
}): string {
|
||||
if (input.cliBranch !== undefined) {
|
||||
return validateBranchName(input.cliBranch, '--default-branch');
|
||||
}
|
||||
if (input.configBranch !== undefined) {
|
||||
return validateBranchName(input.configBranch, `${GITNEXUS_RC_FILENAME} "defaultBranch"`);
|
||||
}
|
||||
const detected = sanitizeDetectedBranch(input.detectedBranch);
|
||||
if (detected) return detected;
|
||||
return DEFAULT_BRANCH_FALLBACK;
|
||||
}
|
||||
|
|
@ -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<void> => {
|
||||
if (options?.verbose) {
|
||||
const analyzeCommandImpl = async (
|
||||
inputPath?: string,
|
||||
cliOptions?: AnalyzeOptions,
|
||||
): Promise<void> => {
|
||||
console.log('\n GitNexus Analyzer\n');
|
||||
|
||||
// ── Resolve the target repo root ──────────────────────────────────
|
||||
// Resolved FIRST because `.gitnexusrc` is read from the repo root (not the
|
||||
// caller's cwd), and config can set defaults that the validation below
|
||||
// consumes. `--skip-git` is a CLI-only flag (never a config key), so the raw
|
||||
// CLI options are authoritative for repo-root resolution.
|
||||
let repoPath: string;
|
||||
if (inputPath) {
|
||||
repoPath = path.resolve(inputPath);
|
||||
} else if (cliOptions?.skipGit) {
|
||||
// --skip-git: treat cwd as the index root, do not walk up to a parent git repo.
|
||||
repoPath = path.resolve(process.cwd());
|
||||
} else {
|
||||
const gitRoot = getGitRoot(process.cwd());
|
||||
if (!gitRoot) {
|
||||
console.log(
|
||||
' Not inside a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
repoPath = gitRoot;
|
||||
}
|
||||
|
||||
const repoHasGit = hasGitDir(repoPath);
|
||||
if (!repoHasGit && !cliOptions?.skipGit) {
|
||||
console.log(
|
||||
' Not a git repository.\n Tip: pass --skip-git to index any folder without a .git directory.\n',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (!repoHasGit) {
|
||||
console.log(
|
||||
' Warning: no .git directory found — commit-tracking and incremental updates disabled.\n',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate an explicit `--default-branch` up front so its errors are
|
||||
// attributed to the flag (with a CLI-specific recovery hint) rather than to
|
||||
// `.gitnexusrc`, which the user may not even have (#1996 tri-review).
|
||||
if (cliOptions?.defaultBranch !== undefined) {
|
||||
try {
|
||||
validateBranchName(cliOptions.defaultBranch, '--default-branch');
|
||||
} catch (err) {
|
||||
cliError(` ${err instanceof Error ? err.message : String(err)}\n`, {
|
||||
recoveryHint: 'default-branch-invalid',
|
||||
});
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Load .gitnexusrc and merge: CLI flags override config (#243) ───
|
||||
// Parse/validate before the progress bar so a malformed config produces an
|
||||
// actionable error and exits before any expensive analysis starts.
|
||||
let options: AnalyzeOptions;
|
||||
let resolvedDefaultBranch: string;
|
||||
try {
|
||||
const fileConfig = loadAnalyzeConfig(repoPath);
|
||||
options = mergeAnalyzeOptions(cliOptions ?? {}, fileConfig);
|
||||
|
||||
// Resolve the default branch threaded into generated context:
|
||||
// CLI --default-branch > .gitnexusrc defaultBranch/branch
|
||||
// > auto-detected origin/HEAD > "main".
|
||||
// Only shell out to git when no branch was configured AND the generated
|
||||
// context will actually use it, keeping the common path free of an extra
|
||||
// git call. Detection is best-effort and never blocks analyze.
|
||||
const cliBranch = cliOptions?.defaultBranch;
|
||||
const configBranch = fileConfig?.defaultBranch;
|
||||
const willGenerateContext = !options.indexOnly && !options.skipAgentsMd;
|
||||
let detectedBranch: string | null = null;
|
||||
if (
|
||||
cliBranch === undefined &&
|
||||
configBranch === undefined &&
|
||||
repoHasGit &&
|
||||
!cliOptions?.skipGit &&
|
||||
willGenerateContext
|
||||
) {
|
||||
try {
|
||||
detectedBranch = getDefaultBranch(repoPath);
|
||||
} catch {
|
||||
detectedBranch = null;
|
||||
}
|
||||
}
|
||||
resolvedDefaultBranch = resolveDefaultBranch({ cliBranch, configBranch, detectedBranch });
|
||||
} catch (err) {
|
||||
const msg =
|
||||
err instanceof GitNexusRcError
|
||||
? err.message
|
||||
: `Invalid .gitnexusrc: ${err instanceof Error ? err.message : String(err)}`;
|
||||
cliError(` ${msg}\n`, { recoveryHint: 'gitnexusrc-invalid' });
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.verbose) {
|
||||
process.env.GITNEXUS_VERBOSE = '1';
|
||||
}
|
||||
|
||||
if (options?.maxFileSize) {
|
||||
if (options.maxFileSize) {
|
||||
process.env.GITNEXUS_MAX_FILE_SIZE = options.maxFileSize;
|
||||
}
|
||||
|
||||
if (options?.workerTimeout) {
|
||||
if (options.workerTimeout) {
|
||||
const workerTimeoutSeconds = Number(options.workerTimeout);
|
||||
if (!Number.isFinite(workerTimeoutSeconds) || workerTimeoutSeconds < 1) {
|
||||
cliError(' --worker-timeout must be at least 1 second.\n');
|
||||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ program
|
|||
'(no-op when --index-only is also set).',
|
||||
)
|
||||
.option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
|
||||
.option(
|
||||
'--default-branch <branch>',
|
||||
'Default branch used in the generated regression-compare example (base_ref). ' +
|
||||
'Falls back to .gitnexusrc, then auto-detected origin/HEAD, then "main".',
|
||||
)
|
||||
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
|
||||
.option(
|
||||
'--skip-skills',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,8 +7,8 @@
|
|||
* call-processor so that the classification logic lives in one place.
|
||||
*
|
||||
* Heritage (mixins: include/extend/prepend) was previously routed here
|
||||
* but is now handled by heritageExtractor.extractFromCall before the
|
||||
* call router runs. The router still returns 'skip' for these calls.
|
||||
* but is now emitted by the scope-resolution pipeline. The router still
|
||||
* returns 'skip' for these calls so they don't become spurious call edges.
|
||||
*
|
||||
* NOTE: This file is intentionally duplicated in gitnexus-web/ because the
|
||||
* two packages have separate build targets (Node native vs WASM/browser).
|
||||
|
|
@ -24,9 +24,10 @@ export type CallRoutingResult = RubyCallRouting | null;
|
|||
|
||||
/**
|
||||
* Per-language call router.
|
||||
* IMPORTANT: Call-routed imports bypass preprocessImportPath(), so any router that
|
||||
* returns an importPath MUST validate it independently (length cap, control-char
|
||||
* rejection). See routeRubyCall for the reference implementation.
|
||||
* IMPORTANT: Call-routed imports are NOT sanitized by the standard import-path
|
||||
* cleaner, so any router that returns an importPath MUST validate it
|
||||
* independently (length cap, control-char rejection). See routeRubyCall for the
|
||||
* reference implementation.
|
||||
*/
|
||||
export type CallRouter = (calledName: string, callNode: SyntaxNode) => CallRoutingResult;
|
||||
|
||||
|
|
@ -82,10 +83,10 @@ export function routeRubyCall(calledName: string, callNode: SyntaxNode): RubyCal
|
|||
return { kind: 'import', importPath, isRelative };
|
||||
}
|
||||
|
||||
// ── include / extend / prepend — heritage (now handled by heritageExtractor) ─
|
||||
// Call-based heritage is intercepted by heritageExtractor.extractFromCall
|
||||
// before the call router runs. Return SKIP_RESULT so these calls don't
|
||||
// fall through to normal call processing.
|
||||
// ── include / extend / prepend — heritage (emitted by scope-resolution) ─
|
||||
// Call-based heritage (Ruby mixins) is emitted by the scope-resolution
|
||||
// pipeline. Return SKIP_RESULT so these calls don't fall through to normal
|
||||
// call processing and become spurious call edges.
|
||||
if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
|
||||
return SKIP_RESULT;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,100 +78,3 @@ export interface CallExtractionConfig {
|
|||
*/
|
||||
typeAsReceiverHeuristic?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call-resolution DAG types
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// The call-resolution pipeline is a typed DAG:
|
||||
//
|
||||
// extract-call ──▶ classify-form ──▶ infer-receiver ──▶ select-dispatch ──▶ resolve-target ──▶ emit-edge
|
||||
//
|
||||
// Provider hooks plug in at infer-receiver and select-dispatch; shared stages
|
||||
// stay language-agnostic. Stages 1-2 run in the parse worker; stages 3-6 run
|
||||
// on the main thread. DAG-internal types below are main-thread-only and never
|
||||
// serialize to the graph.
|
||||
|
||||
/**
|
||||
* DAG stage 3 output: call record with receiver type and source discriminant.
|
||||
*
|
||||
* `receiverTypeName` is resolved via TypeEnv → constructor-map → class-as-receiver →
|
||||
* mixed-chain, or synthesized by `inferImplicitReceiver`. `receiverSource` tags
|
||||
* which path won and drives MRO strategy selection in stage 4.
|
||||
*
|
||||
* Invariants:
|
||||
* - `receiverSource` MUST match how `receiverTypeName` was resolved; every
|
||||
* discriminant must have a live reader and writer.
|
||||
* - `hint` is opaque to shared stages; only the same provider's `selectDispatch` reads it.
|
||||
*
|
||||
* @see language-provider.ts § inferImplicitReceiver, selectDispatch
|
||||
*/
|
||||
export interface ReceiverEnriched {
|
||||
readonly calledName: string;
|
||||
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
||||
readonly receiverName: string | undefined;
|
||||
readonly receiverTypeName: string | undefined;
|
||||
readonly receiverSource:
|
||||
| 'none'
|
||||
| 'typed-binding'
|
||||
| 'constructor-map'
|
||||
| 'class-as-receiver'
|
||||
| 'mixed-chain'
|
||||
| 'implicit-self';
|
||||
/** Free-form hint from the provider hook; opaque to shared stages. */
|
||||
readonly hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider hook output for `LanguageProvider.inferImplicitReceiver` (DAG stage 3).
|
||||
*
|
||||
* Overlay applied to `ReceiverEnriched` when an implicit receiver is synthesized.
|
||||
* Ruby example: bare `serialize` inside `Account#call_serialize` →
|
||||
* `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
|
||||
* receiverSource: 'implicit-self', hint: 'instance' }`
|
||||
*
|
||||
* Invariants:
|
||||
* - `receiverSource` is always `'implicit-self'` — the only variant this type produces.
|
||||
* - `callForm` is always `'member'` — the rewrite converts bare-call to method invocation.
|
||||
* - `hint` is opaque to shared stages; consumed by the same language's `selectDispatch`.
|
||||
*/
|
||||
export interface ImplicitReceiverOverride {
|
||||
readonly callForm: 'free' | 'member' | 'constructor';
|
||||
readonly receiverName: string;
|
||||
readonly receiverTypeName: string;
|
||||
readonly receiverSource: Extract<ReceiverEnriched['receiverSource'], 'implicit-self'>;
|
||||
/** Free-form language tag (e.g. Ruby sets 'singleton' for `def self.foo`
|
||||
* method bodies). Consumed by the same language's `selectDispatch` hook. */
|
||||
readonly hint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DAG stage 4 output: dispatch strategy for resolving the target method.
|
||||
*
|
||||
* Encodes which resolver branch to try first and an optional fallback.
|
||||
* Stage 5 delegates to `resolveMemberCall`, `resolveFreeCall`, or
|
||||
* `resolveStaticCall` based on `primary`.
|
||||
*
|
||||
* - `primary`: `'owner-scoped'` = MRO walk, `'free'` = arity-tiered global lookup,
|
||||
* `'constructor'` = type instantiation.
|
||||
* - `fallback`: Only `'free-arity-narrowed'` exists; used by Ruby implicit-self
|
||||
* to degrade to arity-tiered free lookup when the MRO walk misses.
|
||||
* - `ancestryView`: Ruby `'ruby-mixin'` only. `'singleton'` walks extend providers
|
||||
* only; a miss NEVER falls through to file-scoped lookup (enforced in
|
||||
* resolveCallTarget). `'instance'` is the default.
|
||||
*
|
||||
* Common patterns:
|
||||
* - `{primary: 'constructor'}` — constructor call
|
||||
* - `{primary: 'owner-scoped'}` — member call with known type
|
||||
* - `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}` — Ruby implicit-self
|
||||
* - `{primary: 'owner-scoped', ancestryView: 'singleton'}` — Ruby class-method call
|
||||
*
|
||||
* @see language-provider.ts § selectDispatch
|
||||
* @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
|
||||
*/
|
||||
export interface DispatchDecision {
|
||||
readonly primary: 'owner-scoped' | 'free' | 'constructor';
|
||||
readonly fallback?: 'free-arity-narrowed';
|
||||
readonly ancestryView?: 'instance' | 'singleton';
|
||||
readonly hint?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,10 +45,33 @@ export const cClassConfig: ClassExtractionConfig = {
|
|||
export const cppClassConfig: ClassExtractionConfig = {
|
||||
language: SupportedLanguages.CPlusPlus,
|
||||
typeDeclarationNodes: ['class_specifier', 'struct_specifier', 'enum_specifier'],
|
||||
ancestorScopeNodeTypes: ['namespace_definition', 'class_specifier', 'struct_specifier'],
|
||||
// #1995: `union_specifier` is included so a type nested in a NAMED union
|
||||
// (`union U1 { struct Inner {...} }`) qualifies as `U1.Inner`. Anonymous unions
|
||||
// have no `name` child → extractScopeSegmentsFromNode returns [] → they correctly
|
||||
// contribute nothing (members inject into the enclosing scope). C uses the
|
||||
// separate cClassConfig (no qualifiedNodeId), so it is intentionally untouched.
|
||||
ancestorScopeNodeTypes: [
|
||||
'namespace_definition',
|
||||
'class_specifier',
|
||||
'struct_specifier',
|
||||
'union_specifier',
|
||||
],
|
||||
// #1978: key nested-type nodes by their fully-qualified path (Outer.Inner) so
|
||||
// same-tail nested types in one TU stay distinct instead of silently merging.
|
||||
qualifiedNodeId: true,
|
||||
// #1995: anonymous namespaces have no `name` child, so the generic scope walker
|
||||
// drops them (empty segment) and two `namespace { struct Inner {} }` blocks in one
|
||||
// TU collapse onto a single `Inner` node. Give each anonymous namespace_definition
|
||||
// a deterministic per-block discriminator (its start byte — stable across the
|
||||
// sequential and worker full-file parses) so the nested types stay distinct.
|
||||
// Returning `undefined` for every other scope — named namespaces (incl. `inline
|
||||
// namespace`), classes, structs, named unions — falls through to the default
|
||||
// name-based extraction, leaving them unchanged. Anonymous UNIONS are not matched
|
||||
// here (members inject into the enclosing scope), so they keep yielding [].
|
||||
extractScopeSegments: (node) =>
|
||||
node.type === 'namespace_definition' && !node.childForFieldName?.('name')
|
||||
? [`@anon${node.startIndex}`]
|
||||
: undefined,
|
||||
extractName: (node) => {
|
||||
const nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) return undefined;
|
||||
|
|
|
|||
|
|
@ -164,6 +164,14 @@ export function createClassExtractor(config: ClassExtractionConfig): ClassExtrac
|
|||
return extract(node, { name: simpleName })?.qualifiedName ?? null;
|
||||
},
|
||||
|
||||
// #1991: qualify a non-typeDeclaration scope node (e.g. a Ruby `module` → Trait)
|
||||
// by the same ancestor-scope walk the node-id path uses, so two same-tail nested
|
||||
// mixin modules stay distinct. extract()/extractQualifiedName cannot be reused —
|
||||
// they bail on non-typeDeclarations (a module is not in typeDeclarationNodes).
|
||||
qualifyScopeName(node: SyntaxNode, simpleName: string): string {
|
||||
return buildQualifiedName(node, simpleName);
|
||||
},
|
||||
|
||||
shouldSkipClassCapture(context): boolean {
|
||||
return config.shouldSkipClassCapture?.(context) ?? false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -44,6 +44,14 @@ export interface ClassExtractor {
|
|||
},
|
||||
): ExtractedClassSymbol | null;
|
||||
extractQualifiedName(node: SyntaxNode, simpleName: string): string | null;
|
||||
/**
|
||||
* #1991: qualify a scope-defining node that maps to a class-like registry label
|
||||
* (e.g. a Ruby `module` → Trait) but is NOT a typeDeclaration, so it cannot go
|
||||
* through extract()/extractQualifiedName (which bail on non-typeDeclarations).
|
||||
* Walks the same ancestor scopes as the node-id path. Optional — only providers
|
||||
* that materialize such nodes implement it.
|
||||
*/
|
||||
qualifyScopeName?(node: SyntaxNode, simpleName: string): string;
|
||||
shouldSkipClassCapture?(
|
||||
context: ClassCaptureContext & { nodeLabel: ClassLikeNodeLabel },
|
||||
): boolean;
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export function finalizeScopeModel(
|
|||
|
||||
// ── Step 3: MethodDispatchIndex. Today we lack per-language MRO
|
||||
// strategies wired into this orchestrator (that belongs with the
|
||||
// HeritageMap bridge, a separate piece of work). Ship an EMPTY index
|
||||
// MRO bridge, a separate piece of work). Ship an EMPTY index
|
||||
// so the bundle shape is consistent; the callbacks return `[]` for
|
||||
// every owner and `implementsOf` returns `[]`. Populating this
|
||||
// properly is tracked alongside the per-language provider hooks.
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/cpp.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* C++ base-class supertype shapes (legacy CPP_QUERIES bank only).
|
||||
*
|
||||
* A `base_class_clause` entry can be a bare `type_identifier`, a
|
||||
* `template_type` (`Base<T>`), or a `qualified_identifier` (`ns::Base`,
|
||||
* possibly itself wrapping a `template_type`). Entries may be prefixed by an
|
||||
* `access_specifier`; tree-sitter's bracketed alternation matches the base
|
||||
* node regardless of the preceding access specifier, so a single pattern
|
||||
* covers both `: Base` and `: public Base`.
|
||||
*
|
||||
* NOTE: the registry/scope-resolution path emits its own normalized
|
||||
* `@reference.inherits` captures (see languages/cpp/captures.ts); this
|
||||
* descriptor only repairs the legacy heritage-query bank.
|
||||
*/
|
||||
export const cppHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['type_identifier', 'template_type', 'qualified_identifier'],
|
||||
};
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/csharp.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* C# base-list supertype shapes.
|
||||
*
|
||||
* Every `base_list` entry (class/record/struct/interface) is captured as
|
||||
* `@heritage.extends`; EXTENDS-vs-IMPLEMENTS is decided downstream by
|
||||
* `resolveExtendsType`, so the query does not pre-split. Entries can be a bare
|
||||
* `identifier`, a `generic_name` (`IFoo<T>`), a `qualified_name` (`ns.Base`,
|
||||
* and also `global::System.IDisposable` — a dotted name qualified by an
|
||||
* `alias_qualified_name` parses as a `qualified_name` whose first part is the
|
||||
* alias), an `alias_qualified_name` (a *bare* alias-qualified base with no
|
||||
* dotted suffix: `global::IDisposable`, `MyAlias::Foo`), or a
|
||||
* `primary_constructor_base_type` (`Base(args)` on a record).
|
||||
*
|
||||
* NOTE: `scoped_type` is intentionally NOT listed. Per
|
||||
* tree-sitter-c-sharp/src/node-types.json, `scoped_type` is a `type` subtype
|
||||
* that wraps a `ref_type` (the `scoped ref`/`scoped in` parameter modifier);
|
||||
* it is referenced only by the hidden `type` supertype and never appears as a
|
||||
* `base_list` entry, so adding it would be a dead, untested shape. The
|
||||
* alias-qualified base shapes that *do* occur are `qualified_name` (dotted) and
|
||||
* `alias_qualified_name` (bare) — verified by parsing
|
||||
* `class A : System.Exception, global::System.IDisposable, MyAlias::Foo {}`.
|
||||
* `normalizeSupertypeName` collapses `alias_qualified_name` to the simple name
|
||||
* via its `name` field (an `identifier`). See
|
||||
* `test/integration/heritage-supertype-shapes.test.ts`.
|
||||
*/
|
||||
export const csharpHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: [
|
||||
'identifier',
|
||||
'generic_name',
|
||||
'qualified_name',
|
||||
'alias_qualified_name',
|
||||
'primary_constructor_base_type',
|
||||
],
|
||||
};
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/go.ts
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { HeritageExtractionConfig, SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* Go embed supertype shapes.
|
||||
*
|
||||
* Struct embedding (anonymous `field_declaration` type) and interface-in-
|
||||
* interface embedding (`interface_type → type_elem`) can both name a bare
|
||||
* `type_identifier`, a `qualified_type` (`pkg.Base`), or a `generic_type`
|
||||
* (`Gen[T]`). Named struct fields also match the field pattern and are
|
||||
* filtered out at runtime by {@link goHeritageConfig.shouldSkipExtends}.
|
||||
*/
|
||||
export const goHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['type_identifier', 'qualified_type', 'generic_type'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Go heritage extraction config.
|
||||
*
|
||||
* Go struct embedding: the tree-sitter query matches ALL field_declarations
|
||||
* with type_identifier, but only anonymous fields (no name) are embedded.
|
||||
* Named fields like `Breed string` also match — skip them.
|
||||
*
|
||||
* The shouldSkipExtends hook checks if the extends node's parent is a
|
||||
* field_declaration with a named field child, indicating a regular
|
||||
* (non-embedded) field that should not produce a heritage record.
|
||||
*
|
||||
* It also skips type-set constraint operands. An interface constraint like
|
||||
* `interface { int | float64 }` parses as an `interface_type` containing a
|
||||
* single `type_elem` whose named children are the union operands (`int`,
|
||||
* `float64`), separated by unnamed `|` tokens — each operand reaches its
|
||||
* `type_elem` parent directly via `.parent` (no intermediate binary node).
|
||||
* These operands are NOT embedded supertypes, so a multi-operand `type_elem`
|
||||
* (more than one named child) is skipped.
|
||||
*
|
||||
* Residual: a single-element `type_elem` (`interface { ~int }` or
|
||||
* `interface { SomeConstraint }`) is structurally indistinguishable from a
|
||||
* genuine interface embed by element count alone, so it is left to match. This
|
||||
* is acceptable — a one-element type-set is rare in real Go and a spurious
|
||||
* embed edge to a builtin/constraint name is harmless (it resolves to nothing).
|
||||
*/
|
||||
export const goHeritageConfig: HeritageExtractionConfig = {
|
||||
language: SupportedLanguages.Go,
|
||||
|
||||
shouldSkipExtends(extendsNode) {
|
||||
const parent = extendsNode.parent;
|
||||
if (parent == null) return false;
|
||||
// Named struct field (e.g. `Breed string`) — not an embed.
|
||||
if (parent.type === 'field_declaration' && parent.childForFieldName?.('name') != null) {
|
||||
return true;
|
||||
}
|
||||
// Multi-element interface type-set (`int | float64`) — constraint operands,
|
||||
// not embeds. Single-element type_elem is left to match (see JSDoc residual).
|
||||
if (parent.type === 'type_elem' && parent.namedChildCount > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/java.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* Java supertype shapes.
|
||||
*
|
||||
* A Java extends/implements position (`superclass`, `super_interfaces →
|
||||
* type_list`, and `interface_declaration → extends_interfaces → type_list`)
|
||||
* can hold a bare `type_identifier`, a `generic_type` (`Foo<T>`), or a
|
||||
* `scoped_type_identifier` (`pkg.Foo`). The grammar's `_type` is a hidden
|
||||
* supertype, so the concrete shapes are enumerated rather than matching `_type`.
|
||||
*/
|
||||
export const javaHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['type_identifier', 'generic_type', 'scoped_type_identifier'],
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/javascript.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* JavaScript supertype shapes.
|
||||
*
|
||||
* `class_heritage` directly holds the parent expression: a bare `identifier`
|
||||
* or a `member_expression` (qualified `ns.Base`).
|
||||
*/
|
||||
export const javascriptHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['identifier', 'member_expression'],
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/kotlin.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* Kotlin delegation-specifier supertype shapes.
|
||||
*
|
||||
* The supertype node nested under a `delegation_specifier` is a `user_type`
|
||||
* (`: Bar`), a `constructor_invocation` (`: Bar()`), or an
|
||||
* `explicit_delegation` (`: Bar by baz`). For the delegation form the inner
|
||||
* `user_type` (the `Bar`) is the supertype, not the delegate expression — the
|
||||
* runtime normalizer descends into it.
|
||||
*/
|
||||
export const kotlinHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['user_type', 'constructor_invocation', 'explicit_delegation'],
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/python.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* Python superclass shapes.
|
||||
*
|
||||
* `class_definition → superclasses (argument_list)` entries can be a bare
|
||||
* `identifier`, an `attribute` (qualified `models.Model` — take `.attribute`),
|
||||
* or a `subscript` (`Generic[T]` — take `.value`).
|
||||
*/
|
||||
export const pythonHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['identifier', 'attribute', 'subscript'],
|
||||
};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/ruby.ts
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type {
|
||||
HeritageExtractionConfig,
|
||||
HeritageInfo,
|
||||
SupertypeShapeDescriptor,
|
||||
} from '../../heritage-types.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
|
||||
/**
|
||||
* Ruby `class A < B` superclass shapes, and the class-name shapes for
|
||||
* `class Foo::Bar`. Both positions accept a bare `constant` or a
|
||||
* `scope_resolution` (`Base::Sup` / `Foo::Bar`); the normalizer reduces the
|
||||
* scope_resolution to its trailing constant.
|
||||
*/
|
||||
export const rubyHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['constant', 'scope_resolution'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Maximum parent depth for enclosing class/module walk.
|
||||
* Prevents runaway walks on malformed/deeply-nested ASTs.
|
||||
*/
|
||||
const MAX_PARENT_DEPTH = 50;
|
||||
|
||||
/**
|
||||
* Walk up the AST from a call node to find the enclosing class or module name.
|
||||
* Ruby include/extend/prepend calls must be inside a class or module body.
|
||||
*/
|
||||
function findEnclosingClassName(callNode: SyntaxNode): string | null {
|
||||
let current = callNode.parent;
|
||||
let depth = 0;
|
||||
while (current && ++depth <= MAX_PARENT_DEPTH) {
|
||||
if (current.type === 'class' || current.type === 'module') {
|
||||
const nameNode = current.childForFieldName?.('name');
|
||||
if (nameNode) return nameNode.text;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Ruby heritage call names that express mixin inclusion. */
|
||||
const RUBY_HERITAGE_CALL_NAMES: ReadonlySet<string> = new Set(['include', 'extend', 'prepend']);
|
||||
|
||||
/**
|
||||
* Ruby heritage extraction config.
|
||||
*
|
||||
* Ruby expresses inheritance in two ways, and only one of them has
|
||||
* dedicated tree-sitter heritage captures:
|
||||
*
|
||||
* 1. Class inheritance (`class A < B`) produces standard
|
||||
* `@heritage.extends` captures and flows through the generic
|
||||
* capture-based `extract` hook (not defined here — the factory
|
||||
* handles it).
|
||||
* 2. Mixin calls (`include`/`extend`/`prepend`) have no dedicated
|
||||
* heritage captures; they surface as ordinary call sites. The
|
||||
* `callBasedHeritage` hook below intercepts them before the call
|
||||
* router, absorbing the mixin routing logic that previously lived
|
||||
* in call-routing.ts (routeRubyCall).
|
||||
*/
|
||||
export const rubyHeritageConfig: HeritageExtractionConfig = {
|
||||
language: SupportedLanguages.Ruby,
|
||||
|
||||
callBasedHeritage: {
|
||||
callNames: RUBY_HERITAGE_CALL_NAMES,
|
||||
|
||||
extract(calledName, callNode, _filePath): HeritageInfo[] {
|
||||
const enclosingClass = findEnclosingClassName(callNode);
|
||||
if (!enclosingClass) return [];
|
||||
|
||||
const results: HeritageInfo[] = [];
|
||||
const argList = callNode.childForFieldName?.('arguments');
|
||||
for (const arg of argList?.children ?? []) {
|
||||
if (arg.type === 'constant' || arg.type === 'scope_resolution') {
|
||||
results.push({
|
||||
className: enclosingClass,
|
||||
parentName: arg.text,
|
||||
kind: calledName, // 'include' | 'extend' | 'prepend'
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/rust.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* Rust trait-impl supertype shapes.
|
||||
*
|
||||
* An `impl_item` trait position can be `type_identifier`, `generic_type`
|
||||
* (`Trait<T>`), or `scoped_type_identifier` (`ns::Trait`). The innermost
|
||||
* `name` field is the trait name.
|
||||
*/
|
||||
export const rustHeritageShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['type_identifier', 'generic_type', 'scoped_type_identifier'],
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/configs/typescript.ts
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../../heritage-types.js';
|
||||
|
||||
/**
|
||||
* TypeScript supertype shapes.
|
||||
*
|
||||
* The class `extends_clause` value is an expression — `identifier` or
|
||||
* `member_expression` (qualified `ns.Base`); generics ride a separate
|
||||
* `type_arguments` field, so there is no `generic_type` here. The class
|
||||
* `implements_clause` and the `interface_declaration → extends_type_clause`
|
||||
* use type-space nodes: `type_identifier`, `generic_type`, and
|
||||
* `nested_type_identifier` (`ns.Base`).
|
||||
*/
|
||||
|
||||
/** Shapes valid in a class `extends_clause` value position. */
|
||||
export const typescriptExtendsShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['identifier', 'member_expression'],
|
||||
};
|
||||
|
||||
/** Shapes valid in `implements_clause` / interface `extends_type_clause`. */
|
||||
export const typescriptInterfaceShapes: SupertypeShapeDescriptor = {
|
||||
shapes: ['type_identifier', 'generic_type', 'nested_type_identifier'],
|
||||
};
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/generic.ts
|
||||
|
||||
/**
|
||||
* Generic table-driven heritage extractor factory.
|
||||
*
|
||||
* Follows the same config+factory pattern as method-extractors/generic.ts,
|
||||
* field-extractors/generic.ts, call-extractors/generic.ts, and
|
||||
* variable-extractors/generic.ts.
|
||||
*
|
||||
* Languages with custom extraction hooks (Go: shouldSkipExtends, Ruby:
|
||||
* callBasedHeritage) pass a full HeritageExtractionConfig. Languages
|
||||
* that use the default capture-based extraction can pass just the
|
||||
* SupportedLanguages enum value — no per-language config file needed.
|
||||
*/
|
||||
|
||||
import type { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { CaptureMap } from '../language-provider.js';
|
||||
import type {
|
||||
HeritageExtractionConfig,
|
||||
HeritageExtractor,
|
||||
HeritageExtractorContext,
|
||||
HeritageInfo,
|
||||
} from '../heritage-types.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { normalizeSupertypeName } from './supertype-alternation.js';
|
||||
|
||||
/**
|
||||
* Create a HeritageExtractor from a declarative config or a language enum.
|
||||
*
|
||||
* When a full HeritageExtractionConfig is provided, custom hooks
|
||||
* (shouldSkipExtends, callBasedHeritage) drive the extraction.
|
||||
* When only a SupportedLanguages value is provided, the factory produces
|
||||
* a default extractor that handles the standard @heritage.* captures.
|
||||
*/
|
||||
export function createHeritageExtractor(
|
||||
config: HeritageExtractionConfig | SupportedLanguages,
|
||||
): HeritageExtractor {
|
||||
const actualConfig: HeritageExtractionConfig =
|
||||
typeof config === 'string' ? { language: config } : config;
|
||||
const callNameSet = actualConfig.callBasedHeritage?.callNames;
|
||||
|
||||
return {
|
||||
language: actualConfig.language,
|
||||
|
||||
extract(captureMap: CaptureMap, context: HeritageExtractorContext): HeritageInfo[] {
|
||||
const classNode = captureMap['heritage.class'];
|
||||
if (!classNode) return [];
|
||||
|
||||
// Normalize the declared class name too: most grammars expose a plain
|
||||
// identifier here (text == normalized), but a few use a qualified/scoped
|
||||
// node (e.g. Ruby `class Foo::Bar`), which must collapse to the simple
|
||||
// name so it matches the symbol table.
|
||||
const className = normalizeSupertypeName(classNode);
|
||||
if (!className) return [];
|
||||
const results: HeritageInfo[] = [];
|
||||
|
||||
const extendsNode = captureMap['heritage.extends'];
|
||||
if (extendsNode) {
|
||||
if (!actualConfig.shouldSkipExtends?.(extendsNode)) {
|
||||
const parentName = normalizeSupertypeName(extendsNode);
|
||||
if (parentName) results.push({ className, parentName, kind: 'extends' });
|
||||
}
|
||||
}
|
||||
|
||||
const implementsNode = captureMap['heritage.implements'];
|
||||
if (implementsNode) {
|
||||
const parentName = normalizeSupertypeName(implementsNode);
|
||||
if (parentName) results.push({ className, parentName, kind: 'implements' });
|
||||
}
|
||||
|
||||
const traitNode = captureMap['heritage.trait'];
|
||||
if (traitNode) {
|
||||
const parentName = normalizeSupertypeName(traitNode);
|
||||
if (parentName) results.push({ className, parentName, kind: 'trait-impl' });
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
...(callNameSet
|
||||
? {
|
||||
extractFromCall(
|
||||
calledName: string,
|
||||
callNode: SyntaxNode,
|
||||
context: HeritageExtractorContext,
|
||||
): HeritageInfo[] | null {
|
||||
if (!callNameSet.has(calledName)) return null;
|
||||
return actualConfig.callBasedHeritage!.extract(calledName, callNode, context.filePath);
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-extractors/supertype-alternation.ts
|
||||
|
||||
/**
|
||||
* Shared, language-agnostic heritage supertype handling.
|
||||
*
|
||||
* Two halves of the same contract live here:
|
||||
*
|
||||
* 1. {@link buildSupertypeAlternation} — given a per-language shape descriptor
|
||||
* (the set of tree-sitter node-type shapes a supertype can take), returns
|
||||
* the tree-sitter S-expression alternation fragment that captures any of
|
||||
* them under a single tag, e.g.
|
||||
* `[(type_identifier) (generic_type) (scoped_type_identifier)] @heritage.extends`
|
||||
* Idiomatic tree-sitter alternation is `[(a) (b) (c)]` (one-of), which the
|
||||
* heritage query blocks in tree-sitter-queries.ts interpolate inline.
|
||||
*
|
||||
* 2. {@link normalizeSupertypeName} — given the supertype node that actually
|
||||
* matched, reduces it to the INNERMOST simple identifier. Generics
|
||||
* (`Base<T>`), qualified/scoped names (`pkg.Base`, `ns::Base`), and
|
||||
* delegation wrappers (`Bar by baz`) all collapse to the bare name
|
||||
* (`Base` / `Bar`). This mirrors the C++ registry path
|
||||
* (languages/cpp/captures.ts `extractBaseLookupName`) so that the V1
|
||||
* simple-name `ctx.resolve(name)` contract keeps holding for every
|
||||
* language — no `pkg.Base` or `Base<T>` ever reaches resolution.
|
||||
*
|
||||
* No language names appear in this file. Both functions are parameterized by
|
||||
* node-type data (the descriptor and the matched node's own `.type`), per the
|
||||
* shared-ingestion rule in AGENTS.md.
|
||||
*/
|
||||
|
||||
import type { SupertypeShapeDescriptor } from '../heritage-types.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
|
||||
/**
|
||||
* Build a tree-sitter alternation fragment capturing any of the descriptor's
|
||||
* supertype shapes under `tag`.
|
||||
*
|
||||
* A single shape produces `(shape) @tag`; multiple shapes produce the
|
||||
* bracketed one-of `[(a) (b) …] @tag`. Duplicate shapes are de-duplicated so
|
||||
* callers can compose shape lists freely. The returned string is a fragment
|
||||
* meant to be embedded inside a larger container pattern.
|
||||
*/
|
||||
export function buildSupertypeAlternation(
|
||||
descriptor: SupertypeShapeDescriptor,
|
||||
tag: string,
|
||||
): string {
|
||||
const seen = new Set<string>();
|
||||
const unique: string[] = [];
|
||||
for (const shape of descriptor.shapes) {
|
||||
if (!seen.has(shape)) {
|
||||
seen.add(shape);
|
||||
unique.push(shape);
|
||||
}
|
||||
}
|
||||
if (unique.length === 0) {
|
||||
throw new Error('buildSupertypeAlternation: descriptor has no shapes');
|
||||
}
|
||||
const exprs = unique.map((shape) => `(${shape})`);
|
||||
const oneOf = exprs.length === 1 ? exprs[0] : `[${exprs.join(' ')}]`;
|
||||
return `${oneOf} @${tag}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Field names that, when present, point at the meaningful inner part of a
|
||||
* qualified / generic / scoped / attribute / delegation supertype node. Tried
|
||||
* in order; the first that resolves to a child wins. Mirrors the cpp
|
||||
* `getBaseClassName`/`extractBaseLookupName` field preferences but covers the
|
||||
* union of fields used across grammars:
|
||||
* - name : generic_type, generic_name, scoped_type_identifier,
|
||||
* qualified_name, qualified_identifier, qualified_type,
|
||||
* template_type, nested_type_identifier
|
||||
* - type : Go generic_type, C# primary_constructor_base_type, Rust generic_type
|
||||
* - property : TS/JS member_expression (qualified `ns.Base`)
|
||||
* - attribute : Python attribute (`models.Model`)
|
||||
* - value : Python subscript (`Generic[T]`)
|
||||
*/
|
||||
const INNER_NAME_FIELDS = ['name', 'type', 'property', 'attribute', 'value'] as const;
|
||||
|
||||
/**
|
||||
* Read-only snapshots of the four module-private node-type sets that drive
|
||||
* {@link normalizeSupertypeName}'s branch selection. Exported ONLY so a unit
|
||||
* test can enumerate the real members and assert each one still fires the
|
||||
* branch it documents — a typo'd/removed/extra member would otherwise fall
|
||||
* through silently. Not part of the runtime contract; do not consume in
|
||||
* production code.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const SUPERTYPE_NODE_TYPE_SETS = {
|
||||
innerNameFields: INNER_NAME_FIELDS,
|
||||
get leafTypes(): ReadonlySet<string> {
|
||||
return LEAF_TYPES;
|
||||
},
|
||||
get skippedInnerTypes(): ReadonlySet<string> {
|
||||
return SKIPPED_INNER_TYPES;
|
||||
},
|
||||
get leadingNameTypes(): ReadonlySet<string> {
|
||||
return LEADING_NAME_TYPES;
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** Node types whose own `.text` is already the simple identifier. */
|
||||
const LEAF_TYPES: ReadonlySet<string> = new Set([
|
||||
'type_identifier',
|
||||
'identifier',
|
||||
'constant',
|
||||
'field_identifier',
|
||||
'namespace_identifier',
|
||||
'package_identifier',
|
||||
'simple_identifier',
|
||||
'property_identifier',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Child node types to skip during the children-walk fallback: generic
|
||||
* argument lists (hold type params, not the name) and delegate/call subtrees.
|
||||
*
|
||||
* `value_arguments` covers the Kotlin `constructor_invocation` shape
|
||||
* (`: Bar()` → `constructor_invocation` wrapping `user_type` + `value_arguments`):
|
||||
* the right-to-left walk would otherwise land on the argument list first, so
|
||||
* skipping it lets the walk fall through to the leading `user_type`. This is
|
||||
* the intentional handling for `constructor_invocation` — it is deliberately
|
||||
* NOT a leading-name type (see {@link LEADING_NAME_TYPES}), because its name is
|
||||
* still recovered by the trailing-name walk once the arguments are skipped.
|
||||
*/
|
||||
const SKIPPED_INNER_TYPES: ReadonlySet<string> = new Set([
|
||||
'type_arguments',
|
||||
'type_argument_list',
|
||||
'template_argument_list',
|
||||
'argument_list',
|
||||
'value_arguments',
|
||||
'call_expression',
|
||||
'call_suffix',
|
||||
'annotated_lambda',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Node types whose supertype name is their FIRST named child rather than their
|
||||
* last. The trailing-name walk is correct for qualified/scoped shapes
|
||||
* (qualifier-first, name-last), but some wrappers put the supertype first and a
|
||||
* delegate expression after it.
|
||||
*
|
||||
* Kotlin `explicit_delegation` (`: Bar by baz`, `by baz.qux`, `by baz()`) has
|
||||
* shape `(user_type) (by) (<delegate-expression>)`: the supertype is the
|
||||
* leading `user_type`, and the delegate (which can be an identifier, a
|
||||
* navigation `baz.qux`, or a call `baz()`) trails it. A plain right-to-left
|
||||
* walk would pick the delegate's trailing name (`qux` / `baz`) instead of the
|
||||
* supertype, so these node types recurse into their first named child only.
|
||||
*
|
||||
* Structural, not language-named: any grammar exposing a leading-name wrapper
|
||||
* can be added here.
|
||||
*/
|
||||
const LEADING_NAME_TYPES: ReadonlySet<string> = new Set(['explicit_delegation']);
|
||||
|
||||
/** Guard against pathological/cyclic ASTs while descending into a supertype. */
|
||||
const MAX_NORMALIZE_DEPTH = 24;
|
||||
|
||||
/**
|
||||
* Reduce a matched supertype node to its innermost simple name.
|
||||
*
|
||||
* Strategy (node-type-driven, matching the cpp reference):
|
||||
* 1. Leaf identifier types return `.text` directly.
|
||||
* 2. Try field-based access (name/type/property/attribute/value) and recurse
|
||||
* into the first field that resolves. Some grammars expose the parts only
|
||||
* via fields (Java generic_type→name, Go qualified_type→name, etc.).
|
||||
* 3. Leading-name wrappers ({@link LEADING_NAME_TYPES}, e.g. Kotlin
|
||||
* `explicit_delegation` `Bar by baz`) carry the supertype as their FIRST
|
||||
* named child and a delegate expression after it — recurse into the first
|
||||
* child so the delegate's name never wins.
|
||||
* 4. Fall back to a children walk when fields are empty (e.g. C++
|
||||
* qualified_identifier in 0.23.x can carry the name only as a child). The
|
||||
* LAST named child is preferred because qualified/scoped shapes put the
|
||||
* qualifier first and the actual name last; delegate/argument subtrees
|
||||
* ({@link SKIPPED_INNER_TYPES}) are skipped so e.g. a Kotlin
|
||||
* `constructor_invocation` (`Bar()`) resolves to `Bar`.
|
||||
*/
|
||||
export function normalizeSupertypeName(node: SyntaxNode | null | undefined): string {
|
||||
return normalize(node, 0);
|
||||
}
|
||||
|
||||
function normalize(node: SyntaxNode | null | undefined, depth: number): string {
|
||||
if (!node || depth > MAX_NORMALIZE_DEPTH) return '';
|
||||
|
||||
if (LEAF_TYPES.has(node.type)) {
|
||||
return node.text;
|
||||
}
|
||||
|
||||
// Field-based access first — most grammars expose the inner name via a field.
|
||||
for (const field of INNER_NAME_FIELDS) {
|
||||
const child = node.childForFieldName?.(field);
|
||||
if (child) {
|
||||
const inner = normalize(child, depth + 1);
|
||||
if (inner.length > 0) return inner;
|
||||
}
|
||||
}
|
||||
|
||||
// Leading-name wrappers (e.g. Kotlin `explicit_delegation`: `Bar by baz`)
|
||||
// put the supertype FIRST and a delegate expression after it. Recurse into
|
||||
// the first named child only so we pick `Bar`, never the delegate's name.
|
||||
if (LEADING_NAME_TYPES.has(node.type)) {
|
||||
const first = node.namedChild(0);
|
||||
const inner = normalize(first, depth + 1);
|
||||
if (inner.length > 0) return inner;
|
||||
}
|
||||
|
||||
// Children fallback: walk named children right-to-left so qualified/scoped
|
||||
// shapes (qualifier first, name last) resolve to the trailing name.
|
||||
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
// Skip generic-argument lists and delegate/call subtrees — they hold
|
||||
// type arguments or the delegate expression, not the supertype name.
|
||||
// (Kotlin `constructor_invocation`/`explicit_delegation` wrap the
|
||||
// user_type first and a value_arguments / call_expression second.)
|
||||
if (SKIPPED_INNER_TYPES.has(child.type)) continue;
|
||||
const inner = normalize(child, depth + 1);
|
||||
if (inner.length > 0) return inner;
|
||||
}
|
||||
|
||||
// Last resort: trim obvious generic/qualifier syntax from the raw text so we
|
||||
// never leak `Base<T>` / `pkg.Base` / `ns::Base` to downstream resolution.
|
||||
return simplifyRawName(node.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort textual fallback when the AST shape is unrecognized: drop any
|
||||
* generic argument list and keep the final qualified segment.
|
||||
*
|
||||
* Exported for unit coverage of the raw-name reduction (`Base<T>` → `Base`,
|
||||
* `pkg.Base` → `Base`, `ns::Base` → `Base`).
|
||||
*/
|
||||
export function simplifyRawName(text: string): string {
|
||||
const withoutGenerics = text.replace(/[<\[].*$/s, '').trim();
|
||||
const segments = withoutGenerics.split(/::|\./);
|
||||
return segments[segments.length - 1]?.trim() ?? '';
|
||||
}
|
||||
|
|
@ -1,499 +0,0 @@
|
|||
/**
|
||||
* Heritage Processor
|
||||
*
|
||||
* Extracts class inheritance relationships:
|
||||
* - EXTENDS: Class extends another Class (TS, JS, Python, C#, C++)
|
||||
* - IMPLEMENTS: Class implements an Interface (TS, C#, Java, Kotlin, PHP)
|
||||
*
|
||||
* Languages like C# use a single `base_list` for both class and interface parents.
|
||||
* We resolve the correct edge type by checking the symbol table: if the parent is
|
||||
* registered as an Interface, we emit IMPLEMENTS; otherwise EXTENDS. For unresolved
|
||||
* external symbols, the fallback heuristic is language-gated:
|
||||
* - C# / Java: apply the `I[A-Z]` naming convention (e.g. IDisposable → IMPLEMENTS)
|
||||
* - Swift: default to IMPLEMENTS (protocol conformance is more common than class inheritance)
|
||||
* - All other languages: default to EXTENDS
|
||||
*/
|
||||
|
||||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
import { ASTCache } from './ast-cache.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
||||
import { generateId } from '../../lib/utils.js';
|
||||
import { getLanguageFromFilename, type NodeLabel, type SupportedLanguages } from 'gitnexus-shared';
|
||||
import { isRegistryPrimary } from './registry-primary-flag.js';
|
||||
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
||||
import { yieldToEventLoop } from './utils/event-loop.js';
|
||||
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
|
||||
import { getProvider } from './languages/index.js';
|
||||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import type {
|
||||
ExtractedHeritage,
|
||||
HeritageResolutionStrategy,
|
||||
HeritageStrategyLookup,
|
||||
} from './model/heritage-map.js';
|
||||
import { resolveExtendsType } from './model/heritage-map.js';
|
||||
import type { ResolutionContext } from './model/resolution-context.js';
|
||||
import { TIER_CONFIDENCE } from './model/resolution-context.js';
|
||||
import type { HeritageInfo } from './heritage-types.js';
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
/**
|
||||
* Derive the heritage-resolution strategy for a language from its
|
||||
* `LanguageProvider`. This is the production wiring that `buildHeritageMap`
|
||||
* and the standalone `resolveExtendsType` call site use — the model layer
|
||||
* itself stays unaware of the provider registry.
|
||||
*/
|
||||
export const getHeritageStrategyForLanguage: HeritageStrategyLookup = (
|
||||
lang: SupportedLanguages,
|
||||
): HeritageResolutionStrategy => {
|
||||
const provider = getProvider(lang);
|
||||
return {
|
||||
interfaceNamePattern: provider.interfaceNamePattern,
|
||||
defaultEdge: provider.heritageDefaultEdge ?? 'EXTENDS',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a symbol ID for heritage, with fallback to generated ID.
|
||||
* Uses ctx.resolve() → pick first candidate's nodeId → generate synthetic ID.
|
||||
*/
|
||||
interface ResolvedHeritage {
|
||||
readonly id: string;
|
||||
readonly confidence: number;
|
||||
}
|
||||
|
||||
const resolveHeritageId = (
|
||||
name: string,
|
||||
filePath: string,
|
||||
ctx: ResolutionContext,
|
||||
fallbackLabel: string,
|
||||
fallbackKey?: string,
|
||||
): ResolvedHeritage => {
|
||||
const resolved = ctx.resolve(name, filePath);
|
||||
if (resolved && resolved.candidates.length > 0) {
|
||||
// For global with multiple candidates, refuse (a wrong edge is worse than no edge)
|
||||
if (resolved.tier === 'global' && resolved.candidates.length > 1) {
|
||||
return {
|
||||
id: generateId(fallbackLabel, fallbackKey ?? name),
|
||||
confidence: TIER_CONFIDENCE['global'],
|
||||
};
|
||||
}
|
||||
return { id: resolved.candidates[0].nodeId, confidence: TIER_CONFIDENCE[resolved.tier] };
|
||||
}
|
||||
// Unresolved: use global-tier confidence as fallback
|
||||
return {
|
||||
id: generateId(fallbackLabel, fallbackKey ?? name),
|
||||
confidence: TIER_CONFIDENCE['global'],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a single HeritageInfo to a graph edge, using the same resolution
|
||||
* logic as processHeritageFromExtracted. This bridges the heritage extractor
|
||||
* output format to the graph-resolution side.
|
||||
*/
|
||||
const resolveAndAddHeritageEdge = (
|
||||
graph: KnowledgeGraph,
|
||||
item: HeritageInfo,
|
||||
filePath: string,
|
||||
language: SupportedLanguages,
|
||||
ctx: ResolutionContext,
|
||||
): void => {
|
||||
if (item.kind === 'extends') {
|
||||
const { type: relType, idPrefix } = resolveExtendsType(
|
||||
item.parentName,
|
||||
filePath,
|
||||
ctx,
|
||||
getHeritageStrategyForLanguage(language),
|
||||
);
|
||||
|
||||
const child = resolveHeritageId(
|
||||
item.className,
|
||||
filePath,
|
||||
ctx,
|
||||
'Class',
|
||||
`${filePath}:${item.className}`,
|
||||
);
|
||||
const parent = resolveHeritageId(item.parentName, filePath, ctx, idPrefix);
|
||||
|
||||
if (child.id && parent.id && child.id !== parent.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId(relType, `${child.id}->${parent.id}`),
|
||||
sourceId: child.id,
|
||||
targetId: parent.id,
|
||||
type: relType,
|
||||
confidence: Math.sqrt(child.confidence * parent.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
} else if (item.kind === 'implements') {
|
||||
const cls = resolveHeritageId(
|
||||
item.className,
|
||||
filePath,
|
||||
ctx,
|
||||
'Class',
|
||||
`${filePath}:${item.className}`,
|
||||
);
|
||||
const iface = resolveHeritageId(item.parentName, filePath, ctx, 'Interface');
|
||||
|
||||
if (cls.id && iface.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
|
||||
sourceId: cls.id,
|
||||
targetId: iface.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: Math.sqrt(cls.confidence * iface.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
item.kind === 'trait-impl' ||
|
||||
item.kind === 'include' ||
|
||||
item.kind === 'extend' ||
|
||||
item.kind === 'prepend'
|
||||
) {
|
||||
// Fallback label for an unresolved child name. Rust `trait-impl` children
|
||||
// are structs; Ruby mixin children are classes or modules (Trait). For
|
||||
// Ruby mixin kinds the common case resolves through the type registry
|
||||
// post-plan-001, so the fallback only fires for true-unresolved references
|
||||
// (e.g. mixin inside a singleton_class). `Class` is strictly better than
|
||||
// `Struct` there because it matches the label the structure phase would
|
||||
// emit for a Ruby `class` — the dominant shape. Ruby modules that fail
|
||||
// to resolve still lose their `Trait` label in the synthesized id, but
|
||||
// they fail to resolve rarely and the tradeoff is documented.
|
||||
const childFallbackLabel: NodeLabel = item.kind === 'trait-impl' ? 'Struct' : 'Class';
|
||||
const strct = resolveHeritageId(
|
||||
item.className,
|
||||
filePath,
|
||||
ctx,
|
||||
childFallbackLabel,
|
||||
`${filePath}:${item.className}`,
|
||||
);
|
||||
const trait = resolveHeritageId(item.parentName, filePath, ctx, 'Trait');
|
||||
|
||||
if (strct.id && trait.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}:${item.kind}`),
|
||||
sourceId: strct.id,
|
||||
targetId: trait.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: Math.sqrt(strct.confidence * trait.confidence),
|
||||
reason: item.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const processHeritage = async (
|
||||
graph: KnowledgeGraph,
|
||||
files: { path: string; content: string }[],
|
||||
astCache: ASTCache,
|
||||
ctx: ResolutionContext,
|
||||
onProgress?: (current: number, total: number) => void,
|
||||
) => {
|
||||
const parser = await loadParser();
|
||||
const logSkipped = isVerboseIngestionEnabled();
|
||||
const skippedByLang = logSkipped ? new Map<string, number>() : null;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
onProgress?.(i + 1, files.length);
|
||||
if (i % 20 === 0) await yieldToEventLoop();
|
||||
|
||||
// 1. Check language support
|
||||
const language = getLanguageFromFilename(file.path);
|
||||
if (!language) continue;
|
||||
// Registry-primary gate: the scope-based phase owns inheritance (EXTENDS/
|
||||
// IMPLEMENTS) for this language, so the legacy `@heritage` pass skips it —
|
||||
// mirrors `call-processor`/`import-processor` (#1951).
|
||||
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;
|
||||
|
||||
// 2. Load the language
|
||||
await loadLanguage(language, file.path);
|
||||
|
||||
// 3. Get AST
|
||||
let tree = astCache.get(file.path);
|
||||
if (!tree) {
|
||||
// Use larger bufferSize for files > 32KB
|
||||
// Per-language source preprocessor (length-preserving, e.g. UE macro
|
||||
// stripping for C++). MUST mirror parsing-processor on cache miss so
|
||||
// re-parses see the same input as the cached AST.
|
||||
const parseContent = provider.preprocessSource?.(file.content, file.path) ?? file.content;
|
||||
try {
|
||||
tree = parseSourceSafe(parser, parseContent, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(parseContent),
|
||||
});
|
||||
} catch (parseError) {
|
||||
// Skip files that can't be parsed
|
||||
continue;
|
||||
}
|
||||
// Cache re-parsed tree for potential future use
|
||||
astCache.set(file.path, tree);
|
||||
}
|
||||
|
||||
let query;
|
||||
let matches;
|
||||
try {
|
||||
const treeSitterLang = parser.getLanguage();
|
||||
query = new Parser.Query(treeSitterLang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
} catch (queryError) {
|
||||
logger.warn({ queryError }, `Heritage query error for ${file.path}:`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Process heritage matches via provider heritage extractor
|
||||
const heritageExtractor = provider.heritageExtractor;
|
||||
matches.forEach((match) => {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach((c) => {
|
||||
captureMap[c.name] = c.node;
|
||||
});
|
||||
|
||||
if (!captureMap['heritage.class']) return;
|
||||
if (!heritageExtractor) return;
|
||||
|
||||
const heritageItems = heritageExtractor.extract(captureMap, {
|
||||
filePath: file.path,
|
||||
language,
|
||||
});
|
||||
|
||||
for (const item of heritageItems) {
|
||||
resolveAndAddHeritageEdge(graph, item, file.path, language, ctx);
|
||||
}
|
||||
});
|
||||
|
||||
// Tree is now owned by the LRU cache — no manual delete needed
|
||||
}
|
||||
|
||||
if (skippedByLang && skippedByLang.size > 0) {
|
||||
for (const [lang, count] of skippedByLang.entries()) {
|
||||
logger.warn(
|
||||
`[ingestion] Skipped ${count} ${lang} file(s) in heritage processing — ${lang} parser not available.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fast path: resolve pre-extracted heritage from workers.
|
||||
* No AST parsing — workers already extracted className + parentName + kind.
|
||||
*/
|
||||
export const processHeritageFromExtracted = async (
|
||||
graph: KnowledgeGraph,
|
||||
extractedHeritage: ExtractedHeritage[],
|
||||
ctx: ResolutionContext,
|
||||
onProgress?: (current: number, total: number) => void,
|
||||
) => {
|
||||
const total = extractedHeritage.length;
|
||||
|
||||
for (let i = 0; i < extractedHeritage.length; i++) {
|
||||
if (i % 500 === 0) {
|
||||
onProgress?.(i, total);
|
||||
await yieldToEventLoop();
|
||||
}
|
||||
|
||||
const h = extractedHeritage[i];
|
||||
|
||||
if (h.kind === 'extends') {
|
||||
const fileLanguage = getLanguageFromFilename(h.filePath);
|
||||
if (!fileLanguage) continue;
|
||||
const { type: relType, idPrefix } = resolveExtendsType(
|
||||
h.parentName,
|
||||
h.filePath,
|
||||
ctx,
|
||||
getHeritageStrategyForLanguage(fileLanguage),
|
||||
);
|
||||
|
||||
const child = resolveHeritageId(
|
||||
h.className,
|
||||
h.filePath,
|
||||
ctx,
|
||||
'Class',
|
||||
`${h.filePath}:${h.className}`,
|
||||
);
|
||||
const parent = resolveHeritageId(h.parentName, h.filePath, ctx, idPrefix);
|
||||
|
||||
if (child.id && parent.id && child.id !== parent.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId(relType, `${child.id}->${parent.id}`),
|
||||
sourceId: child.id,
|
||||
targetId: parent.id,
|
||||
type: relType,
|
||||
confidence: Math.sqrt(child.confidence * parent.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
} else if (h.kind === 'implements') {
|
||||
const cls = resolveHeritageId(
|
||||
h.className,
|
||||
h.filePath,
|
||||
ctx,
|
||||
'Class',
|
||||
`${h.filePath}:${h.className}`,
|
||||
);
|
||||
const iface = resolveHeritageId(h.parentName, h.filePath, ctx, 'Interface');
|
||||
|
||||
if (cls.id && iface.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${cls.id}->${iface.id}`),
|
||||
sourceId: cls.id,
|
||||
targetId: iface.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: Math.sqrt(cls.confidence * iface.confidence),
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
h.kind === 'trait-impl' ||
|
||||
h.kind === 'include' ||
|
||||
h.kind === 'extend' ||
|
||||
h.kind === 'prepend'
|
||||
) {
|
||||
// See the per-item call above (processHeritageFromExtractedItem) for
|
||||
// rationale: `Class` is the correct fallback for Ruby mixin kinds,
|
||||
// `Struct` stays the Rust `trait-impl` default.
|
||||
const childFallbackLabel: NodeLabel = h.kind === 'trait-impl' ? 'Struct' : 'Class';
|
||||
const strct = resolveHeritageId(
|
||||
h.className,
|
||||
h.filePath,
|
||||
ctx,
|
||||
childFallbackLabel,
|
||||
`${h.filePath}:${h.className}`,
|
||||
);
|
||||
const trait = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
|
||||
|
||||
if (strct.id && trait.id) {
|
||||
graph.addRelationship({
|
||||
id: generateId('IMPLEMENTS', `${strct.id}->${trait.id}:${h.kind}`),
|
||||
sourceId: strct.id,
|
||||
targetId: trait.id,
|
||||
type: 'IMPLEMENTS',
|
||||
confidence: Math.sqrt(strct.confidence * trait.confidence),
|
||||
reason: h.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(total, total);
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk source files with the same heritage captures as parse-worker, producing
|
||||
* {@link ExtractedHeritage} rows without mutating the graph. Used on the
|
||||
* sequential pipeline path so `buildHeritageMap(..., ctx)` can run before
|
||||
* `processCalls` (worker path defers calls until heritage from all chunks exists).
|
||||
*
|
||||
* This prepass extracts BOTH capture-based heritage (`@heritage.*` — extends /
|
||||
* implements / trait-impl) AND call-based heritage (`@call.name` routed through
|
||||
* `heritageExtractor.extractFromCall` — Ruby `include` / `extend` / `prepend`).
|
||||
* Without the second pass, sequential-mode `sequentialHeritageMap` would not
|
||||
* know about Ruby mixin ancestry before `processCalls` resolves calls against
|
||||
* it, silently dropping mixed-in methods from the graph. This function stays
|
||||
* read-only — `processCalls` still owns emission of heritage graph edges via
|
||||
* its `rubyHeritage` return path.
|
||||
*/
|
||||
export async function extractExtractedHeritageFromFiles(
|
||||
files: { path: string; content: string }[],
|
||||
astCache: ASTCache,
|
||||
): Promise<ExtractedHeritage[]> {
|
||||
const parser = await loadParser();
|
||||
const out: ExtractedHeritage[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const language = getLanguageFromFilename(file.path);
|
||||
if (!language || !isLanguageAvailable(language)) 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 {
|
||||
continue;
|
||||
}
|
||||
astCache.set(file.path, tree);
|
||||
}
|
||||
|
||||
let matches;
|
||||
try {
|
||||
const lang = parser.getLanguage();
|
||||
const query = new Parser.Query(lang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
const callBasedEnabled = !!provider.heritageExtractor?.extractFromCall;
|
||||
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach((c) => {
|
||||
captureMap[c.name] = c.node;
|
||||
});
|
||||
|
||||
if (captureMap['heritage.class']) {
|
||||
if (provider.heritageExtractor) {
|
||||
const heritageItems = provider.heritageExtractor.extract(captureMap, {
|
||||
filePath: file.path,
|
||||
language,
|
||||
});
|
||||
for (const item of heritageItems) {
|
||||
out.push({
|
||||
filePath: file.path,
|
||||
className: item.className,
|
||||
parentName: item.parentName,
|
||||
kind: item.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Call-based heritage (e.g. Ruby include/extend/prepend). Matches the
|
||||
// routing the worker path performs inline in parse-worker.ts — see the
|
||||
// `provider.heritageExtractor?.extractFromCall` branch there. We only
|
||||
// need call-based records here; other @call captures are consumed by
|
||||
// processCalls later in the sequential loop.
|
||||
if (callBasedEnabled && captureMap['call'] && captureMap['call.name']) {
|
||||
const calledName: string = captureMap['call.name'].text;
|
||||
const heritageItems = provider.heritageExtractor!.extractFromCall!(
|
||||
calledName,
|
||||
captureMap['call'],
|
||||
{ filePath: file.path, language },
|
||||
);
|
||||
if (heritageItems) {
|
||||
for (const item of heritageItems) {
|
||||
out.push({
|
||||
filePath: file.path,
|
||||
className: item.className,
|
||||
parentName: item.parentName,
|
||||
kind: item.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
// gitnexus/src/core/ingestion/heritage-types.ts
|
||||
|
||||
/**
|
||||
* Types for the language-agnostic heritage extraction pipeline.
|
||||
*
|
||||
* Follows the same pattern as call-types.ts, variable-types.ts, and
|
||||
* method-types.ts: defines the domain interfaces consumed by
|
||||
* createHeritageExtractor() and the per-language configs.
|
||||
*
|
||||
* Heritage extraction handles extends/implements/trait-impl captures from
|
||||
* tree-sitter queries, plus call-based heritage for languages like Ruby
|
||||
* (include/extend/prepend expressed as method calls).
|
||||
*/
|
||||
|
||||
import type { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type { SyntaxNode } from './utils/ast-helpers.js';
|
||||
import type { CaptureMap } from './language-provider.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extracted result
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Per-match heritage extraction result. The parse worker adds filePath to
|
||||
* produce the final {@link ExtractedHeritage} that enters the resolution
|
||||
* pipeline (heritage-processor.ts / heritage-map.ts).
|
||||
*/
|
||||
export interface HeritageInfo {
|
||||
className: string;
|
||||
parentName: string;
|
||||
/** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */
|
||||
kind: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface HeritageExtractorContext {
|
||||
filePath: string;
|
||||
language: SupportedLanguages;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extractor interface (produced by createHeritageExtractor)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface HeritageExtractor {
|
||||
readonly language: SupportedLanguages;
|
||||
|
||||
/**
|
||||
* Extract heritage records from tree-sitter @heritage.* captures.
|
||||
*
|
||||
* @param captureMap The capture map from a single tree-sitter match
|
||||
* @param context File path and language context
|
||||
* @returns Array of heritage records (may be empty if captures don't match)
|
||||
*/
|
||||
extract(captureMap: CaptureMap, context: HeritageExtractorContext): HeritageInfo[];
|
||||
|
||||
/**
|
||||
* Extract heritage from a call node (for languages where heritage is
|
||||
* expressed as method calls, e.g., Ruby include/extend/prepend).
|
||||
*
|
||||
* @param calledName The method name (e.g. 'include', 'extend', 'prepend')
|
||||
* @param callNode The tree-sitter call AST node
|
||||
* @param context File path and language context
|
||||
* @returns Heritage records if the call is heritage-related, or null to
|
||||
* fall through to the call router / normal call handling.
|
||||
*/
|
||||
extractFromCall?(
|
||||
calledName: string,
|
||||
callNode: SyntaxNode,
|
||||
context: HeritageExtractorContext,
|
||||
): HeritageInfo[] | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config interface (one per language / language group)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Declarative description of the supertype node-type shapes a language can
|
||||
* place in a heritage position (extends / implements / trait / struct embed).
|
||||
*
|
||||
* The query builder (supertype-alternation.ts) turns the `shapes` list into a
|
||||
* tree-sitter alternation fragment `[(shape1) (shape2) …] @<tag>` that is
|
||||
* interpolated into the language's heritage query blocks in
|
||||
* tree-sitter-queries.ts. The runtime name-normalizer
|
||||
* (normalizeSupertypeName) walks whichever shape actually matched and reduces
|
||||
* it to the innermost simple identifier so downstream `ctx.resolve(name)`
|
||||
* keeps working.
|
||||
*
|
||||
* Shapes are bare tree-sitter node-type names (e.g. 'type_identifier',
|
||||
* 'generic_type', 'scoped_type_identifier'). No language names appear here —
|
||||
* the descriptor is parameterized data, consumed by language-agnostic code.
|
||||
*/
|
||||
export interface SupertypeShapeDescriptor {
|
||||
/** Tree-sitter node-type names that may appear in a supertype position. */
|
||||
readonly shapes: readonly string[];
|
||||
}
|
||||
|
||||
export interface HeritageExtractionConfig {
|
||||
language: SupportedLanguages;
|
||||
|
||||
/**
|
||||
* Called for heritage.extends captures. Return true to skip this extends
|
||||
* capture. Used by Go to skip named struct fields that match the
|
||||
* field_declaration pattern but are not anonymous embeddings.
|
||||
*
|
||||
* Default: never skip (all extends captures are valid).
|
||||
*/
|
||||
shouldSkipExtends?: (extendsNode: SyntaxNode) => boolean;
|
||||
|
||||
/**
|
||||
* Call-based heritage extraction for languages where heritage is expressed
|
||||
* as method calls (e.g., Ruby include/extend/prepend).
|
||||
*
|
||||
* callNames: set of method names that trigger heritage extraction.
|
||||
* extract: extract heritage items from the call node + method name.
|
||||
*/
|
||||
callBasedHeritage?: {
|
||||
readonly callNames: ReadonlySet<string>;
|
||||
extract(calledName: string, callNode: SyntaxNode, filePath: string): HeritageInfo[];
|
||||
};
|
||||
}
|
||||
|
|
@ -1,512 +0,0 @@
|
|||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
import { ASTCache } from './ast-cache.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
||||
import { getProvider, getProviderForFile, providersWithImplicitWiring } from './languages/index.js';
|
||||
import type { LanguageProvider } from './language-provider.js';
|
||||
import { generateId } from '../../lib/utils.js';
|
||||
import { getLanguageFromFilename } from 'gitnexus-shared';
|
||||
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
||||
import { yieldToEventLoop } from './utils/event-loop.js';
|
||||
import { parseSourceSafe, parseHadErrors } from '../tree-sitter/safe-parse.js';
|
||||
import type { ExtractedImport } from './workers/parse-worker.js';
|
||||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import { loadImportConfigs } from './language-config.js';
|
||||
import { buildSuffixIndex } from './import-resolvers/utils.js';
|
||||
import type {
|
||||
ResolutionContext,
|
||||
ModuleAliasMap,
|
||||
NamedImportMap,
|
||||
} from './model/resolution-context.js';
|
||||
import type {
|
||||
ImportResult,
|
||||
ResolveCtx,
|
||||
ImportResolutionContext,
|
||||
} from './import-resolvers/types.js';
|
||||
import type { NamedBinding } from './named-bindings/types.js';
|
||||
import type { SyntaxNode } from './utils/ast-helpers.js';
|
||||
import { isDev } from './utils/env.js';
|
||||
import { isRegistryPrimary } from './registry-primary-flag.js';
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
// Type: Map<FilePath, Set<ResolvedFilePath>>
|
||||
// Stores all files that a given file imports from
|
||||
export type ImportMap = Map<string, Set<string>>;
|
||||
|
||||
/** Group files by provider (only those with implicit import wiring), then call each wirer
|
||||
* with its own language's files. O(n) over files, O(1) per provider lookup. */
|
||||
function wireImplicitImports(
|
||||
files: string[],
|
||||
importMap: Map<string, Set<string>>,
|
||||
addImportEdge: (src: string, target: string) => void,
|
||||
projectConfig: unknown,
|
||||
): void {
|
||||
if (providersWithImplicitWiring.length === 0) return;
|
||||
|
||||
const grouped = new Map<LanguageProvider, string[]>();
|
||||
for (const file of files) {
|
||||
const provider = getProviderForFile(file);
|
||||
if (!provider?.implicitImportWirer) continue;
|
||||
let list = grouped.get(provider);
|
||||
if (!list) {
|
||||
list = [];
|
||||
grouped.set(provider, list);
|
||||
}
|
||||
list.push(file);
|
||||
}
|
||||
|
||||
for (const [provider, langFiles] of grouped) {
|
||||
if (langFiles.length > 1) {
|
||||
provider.implicitImportWirer(langFiles, importMap, addImportEdge, projectConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Type: Map<FilePath, Set<PackageDirSuffix>>
|
||||
// Stores Go package directory suffixes imported by a file (e.g., "/internal/auth/").
|
||||
// Avoids expanding every Go package import into N individual ImportMap edges.
|
||||
export type PackageMap = Map<string, Set<string>>;
|
||||
|
||||
// ImportResolutionContext is defined in ./import-resolvers/types.ts — re-exported here for consumers.
|
||||
|
||||
export function buildImportResolutionContext(allPaths: string[]): ImportResolutionContext {
|
||||
const allFileList = allPaths;
|
||||
const normalizedFileList = allFileList.map((p) => p.replace(/\\/g, '/'));
|
||||
const allFilePaths = new Set(allFileList);
|
||||
const index = buildSuffixIndex(normalizedFileList, allFileList);
|
||||
return { allFilePaths, allFileList, normalizedFileList, index, resolveCache: new Map() };
|
||||
}
|
||||
|
||||
// Config loaders extracted to ./language-config.ts (Phase 2 refactor)
|
||||
// Resolver types are in ./import-resolvers/types.ts; named binding types in ./named-bindings/types.ts
|
||||
|
||||
// ============================================================================
|
||||
// Import path preprocessing
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Clean and preprocess a raw import source text into a resolved import path.
|
||||
* Strips quotes/angle brackets (universal) and applies provider-specific
|
||||
* transformations (currently only Kotlin wildcard import detection).
|
||||
*/
|
||||
export function preprocessImportPath(
|
||||
sourceText: string,
|
||||
importNode: SyntaxNode,
|
||||
provider: LanguageProvider,
|
||||
): string | null {
|
||||
const cleaned = sourceText.replace(/['"<>]/g, '');
|
||||
// Defense-in-depth: reject null bytes and control characters (matches Ruby call-routing pattern)
|
||||
if (!cleaned || cleaned.length > 2048 || /[\x00-\x1f]/.test(cleaned)) return null;
|
||||
if (provider.importPathPreprocessor) {
|
||||
return provider.importPathPreprocessor(cleaned, importNode);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/** Create IMPORTS edge helpers that share a resolved-count tracker. */
|
||||
function createImportEdgeHelpers(graph: KnowledgeGraph, importMap: ImportMap) {
|
||||
let totalImportsResolved = 0;
|
||||
|
||||
const addImportGraphEdge = (filePath: string, resolvedPath: string) => {
|
||||
const language = getLanguageFromFilename(filePath);
|
||||
if (language !== null && isRegistryPrimary(language)) return;
|
||||
const sourceId = generateId('File', filePath);
|
||||
const targetId = generateId('File', resolvedPath);
|
||||
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
||||
totalImportsResolved++;
|
||||
graph.addRelationship({
|
||||
id: relId,
|
||||
sourceId,
|
||||
targetId,
|
||||
type: 'IMPORTS',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
};
|
||||
|
||||
const addImportEdge = (filePath: string, resolvedPath: string) => {
|
||||
addImportGraphEdge(filePath, resolvedPath);
|
||||
if (!importMap.has(filePath)) importMap.set(filePath, new Set());
|
||||
importMap.get(filePath)!.add(resolvedPath);
|
||||
};
|
||||
|
||||
return { addImportEdge, addImportGraphEdge, getResolvedCount: () => totalImportsResolved };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply an ImportResult: emit graph edges and update ImportMap/PackageMap.
|
||||
* If namedBindings are provided and the import resolves to a single file,
|
||||
* also populate the NamedImportMap for precise Tier 2a resolution.
|
||||
* Bindings tagged with `isModuleAlias` are routed to moduleAliasMap instead.
|
||||
*/
|
||||
function applyImportResult(
|
||||
result: ImportResult,
|
||||
filePath: string,
|
||||
importMap: ImportMap,
|
||||
packageMap: PackageMap | undefined,
|
||||
addImportEdge: (from: string, to: string) => void,
|
||||
addImportGraphEdge: (from: string, to: string) => void,
|
||||
namedBindings?: NamedBinding[],
|
||||
namedImportMap?: NamedImportMap,
|
||||
moduleAliasMap?: ModuleAliasMap,
|
||||
): void {
|
||||
if (!result) return;
|
||||
|
||||
if (result.kind === 'package' && packageMap) {
|
||||
// Store directory suffix in PackageMap (skip ImportMap expansion)
|
||||
for (const resolvedFile of result.files) {
|
||||
addImportGraphEdge(filePath, resolvedFile);
|
||||
}
|
||||
if (!packageMap.has(filePath)) packageMap.set(filePath, new Set());
|
||||
packageMap.get(filePath)!.add(result.dirSuffix);
|
||||
} else {
|
||||
// 'files' kind, or 'package' without PackageMap — use ImportMap directly
|
||||
const files = result.files;
|
||||
for (const resolvedFile of files) {
|
||||
addImportEdge(filePath, resolvedFile);
|
||||
}
|
||||
|
||||
// Route module aliases (import X as Y) directly to moduleAliasMap.
|
||||
// These are module-level aliases, not symbol bindings — they don't belong in namedImportMap.
|
||||
if (namedBindings && moduleAliasMap && files.length === 1) {
|
||||
const resolvedFile = files[0];
|
||||
for (const binding of namedBindings) {
|
||||
if (!binding.isModuleAlias) continue;
|
||||
let aliasMap = moduleAliasMap.get(filePath);
|
||||
if (!aliasMap) {
|
||||
aliasMap = new Map();
|
||||
moduleAliasMap.set(filePath, aliasMap);
|
||||
}
|
||||
aliasMap.set(binding.local, resolvedFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Record named bindings for precise Tier 2a resolution.
|
||||
// If the same local name is imported from multiple files (e.g., Java static imports
|
||||
// of overloaded methods), remove the entry so resolution falls through to Tier 2a
|
||||
// import-scoped which sees all candidates and can apply arity narrowing.
|
||||
if (namedBindings && namedImportMap) {
|
||||
if (!namedImportMap.has(filePath)) namedImportMap.set(filePath, new Map());
|
||||
const fileBindings = namedImportMap.get(filePath)!;
|
||||
|
||||
if (files.length === 1) {
|
||||
const resolvedFile = files[0];
|
||||
for (const binding of namedBindings) {
|
||||
if (binding.isModuleAlias) continue; // already routed to moduleAliasMap
|
||||
const existing = fileBindings.get(binding.local);
|
||||
if (existing && existing.sourcePath !== resolvedFile) {
|
||||
fileBindings.delete(binding.local);
|
||||
} else {
|
||||
fileBindings.set(binding.local, {
|
||||
sourcePath: resolvedFile,
|
||||
exportedName: binding.exported,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Multi-file resolution (e.g., Rust `use crate::models::{User, Repo}`).
|
||||
// Match each binding to a resolved file by comparing the lowercase binding name
|
||||
// to the file's basename (without extension). If no match, skip the binding.
|
||||
for (const binding of namedBindings) {
|
||||
if (binding.isModuleAlias) continue;
|
||||
const lowerName = binding.exported.toLowerCase();
|
||||
const matchedFile = files.find((f) => {
|
||||
const base = f.replace(/\\/g, '/').split('/').pop() ?? '';
|
||||
const nameWithoutExt = base.substring(0, base.lastIndexOf('.')).toLowerCase();
|
||||
return nameWithoutExt === lowerName;
|
||||
});
|
||||
if (matchedFile) {
|
||||
const existing = fileBindings.get(binding.local);
|
||||
if (existing && existing.sourcePath !== matchedFile) {
|
||||
fileBindings.delete(binding.local);
|
||||
} else {
|
||||
fileBindings.set(binding.local, {
|
||||
sourcePath: matchedFile,
|
||||
exportedName: binding.exported,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN IMPORT PROCESSOR
|
||||
// ============================================================================
|
||||
|
||||
export const processImports = async (
|
||||
graph: KnowledgeGraph,
|
||||
files: { path: string; content: string }[],
|
||||
astCache: ASTCache,
|
||||
ctx: ResolutionContext,
|
||||
onProgress?: (current: number, total: number) => void,
|
||||
repoRoot?: string,
|
||||
allPaths?: string[],
|
||||
) => {
|
||||
const importMap = ctx.importMap;
|
||||
const packageMap = ctx.packageMap;
|
||||
const namedImportMap = ctx.namedImportMap;
|
||||
const moduleAliasMap = ctx.moduleAliasMap;
|
||||
// Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
|
||||
const allFileList = allPaths ?? files.map((f) => f.path);
|
||||
const allFilePaths = new Set(allFileList);
|
||||
const parser = await loadParser();
|
||||
const logSkipped = isVerboseIngestionEnabled();
|
||||
const skippedByLang = logSkipped ? new Map<string, number>() : null;
|
||||
const resolveCache = new Map<string, string | null>();
|
||||
// Pre-compute normalized file list once (forward slashes)
|
||||
const normalizedFileList = allFileList.map((p) => p.replace(/\\/g, '/'));
|
||||
// Build suffix index for O(1) lookups
|
||||
const index = buildSuffixIndex(normalizedFileList, allFileList);
|
||||
|
||||
// Track import statistics
|
||||
let totalImportsFound = 0;
|
||||
|
||||
// Load language-specific configs once before the file loop
|
||||
const configs = await loadImportConfigs(repoRoot || '');
|
||||
const resolveCtx: ResolveCtx = {
|
||||
allFilePaths,
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index,
|
||||
resolveCache,
|
||||
configs,
|
||||
};
|
||||
const { addImportEdge, addImportGraphEdge, getResolvedCount } = createImportEdgeHelpers(
|
||||
graph,
|
||||
importMap,
|
||||
);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
onProgress?.(i + 1, files.length);
|
||||
if (i % 20 === 0) await yieldToEventLoop();
|
||||
|
||||
// 1. Check language support first
|
||||
const language = getLanguageFromFilename(file.path);
|
||||
if (!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;
|
||||
|
||||
// 2. ALWAYS load the language before querying (parser is stateful)
|
||||
await loadLanguage(language, file.path);
|
||||
|
||||
// 3. Get AST (Try Cache First)
|
||||
let tree = astCache.get(file.path);
|
||||
let wasReparsed = false;
|
||||
|
||||
if (!tree) {
|
||||
const parseContent = provider.preprocessSource?.(file.content, file.path) ?? file.content;
|
||||
try {
|
||||
tree = parseSourceSafe(parser, parseContent, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(parseContent),
|
||||
});
|
||||
} catch (parseError) {
|
||||
continue;
|
||||
}
|
||||
wasReparsed = true;
|
||||
// Cache re-parsed tree so call/heritage phases get hits
|
||||
astCache.set(file.path, tree);
|
||||
}
|
||||
|
||||
let query;
|
||||
let matches;
|
||||
try {
|
||||
const lang = parser.getLanguage();
|
||||
query = new Parser.Query(lang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
} catch (queryError: any) {
|
||||
if (isDev) {
|
||||
logger.error(
|
||||
{
|
||||
file: file.path,
|
||||
language,
|
||||
err: queryError?.message || queryError,
|
||||
queryPreview: queryStr.substring(0, 200) + '...',
|
||||
contentPreview: file.content.substring(0, 300),
|
||||
astRootType: tree.rootNode?.type,
|
||||
astHasError: parseHadErrors(tree),
|
||||
},
|
||||
'tree-sitter query error',
|
||||
);
|
||||
}
|
||||
|
||||
if (wasReparsed) (tree as unknown as { delete?: () => void }).delete?.();
|
||||
continue;
|
||||
}
|
||||
|
||||
matches.forEach((match) => {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
||||
|
||||
if (captureMap['import']) {
|
||||
const sourceNode = captureMap['import.source'];
|
||||
if (!sourceNode) {
|
||||
if (isDev) {
|
||||
logger.info(`⚠️ Import captured but no source node in ${file.path}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const rawImportPath = preprocessImportPath(sourceNode.text, captureMap['import'], provider);
|
||||
if (!rawImportPath) return;
|
||||
totalImportsFound++;
|
||||
|
||||
const result = provider.importResolver(rawImportPath, file.path, resolveCtx);
|
||||
const extractor = provider.namedBindingExtractor;
|
||||
const bindings = namedImportMap && extractor ? extractor(captureMap['import']) : undefined;
|
||||
applyImportResult(
|
||||
result,
|
||||
file.path,
|
||||
importMap,
|
||||
packageMap,
|
||||
addImportEdge,
|
||||
addImportGraphEdge,
|
||||
bindings,
|
||||
namedImportMap,
|
||||
moduleAliasMap,
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Language-specific call-as-import routing (Ruby require, etc.) ----
|
||||
if (captureMap['call']) {
|
||||
const callNameNode = captureMap['call.name'];
|
||||
if (callNameNode) {
|
||||
const routed = provider.callRouter?.(callNameNode.text, captureMap['call']);
|
||||
if (routed && routed.kind === 'import') {
|
||||
totalImportsFound++;
|
||||
const result = provider.importResolver(routed.importPath, file.path, resolveCtx);
|
||||
applyImportResult(
|
||||
result,
|
||||
file.path,
|
||||
importMap,
|
||||
packageMap,
|
||||
addImportEdge,
|
||||
addImportGraphEdge,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tree is now owned by the LRU cache — no manual delete needed
|
||||
}
|
||||
|
||||
wireImplicitImports(allFileList, importMap, addImportEdge, configs);
|
||||
|
||||
if (skippedByLang && skippedByLang.size > 0) {
|
||||
for (const [lang, count] of skippedByLang.entries()) {
|
||||
logger.warn(
|
||||
`[ingestion] Skipped ${count} ${lang} file(s) in import processing — ${lang} parser not available.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
logger.info(
|
||||
`📊 Import processing complete: ${getResolvedCount()}/${totalImportsFound} imports resolved to graph edges`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// FAST PATH: Resolve pre-extracted imports (no parsing needed)
|
||||
// ============================================================================
|
||||
|
||||
export const processImportsFromExtracted = async (
|
||||
graph: KnowledgeGraph,
|
||||
files: { path: string }[],
|
||||
extractedImports: ExtractedImport[],
|
||||
ctx: ResolutionContext,
|
||||
onProgress?: (current: number, total: number) => void,
|
||||
repoRoot?: string,
|
||||
prebuiltCtx?: ImportResolutionContext,
|
||||
) => {
|
||||
const importMap = ctx.importMap;
|
||||
const packageMap = ctx.packageMap;
|
||||
const namedImportMap = ctx.namedImportMap;
|
||||
const moduleAliasMap = ctx.moduleAliasMap;
|
||||
const importCtx = prebuiltCtx ?? buildImportResolutionContext(files.map((f) => f.path));
|
||||
const { allFilePaths, allFileList, normalizedFileList, index, resolveCache } = importCtx;
|
||||
|
||||
let totalImportsFound = 0;
|
||||
|
||||
const configs = await loadImportConfigs(repoRoot || '');
|
||||
const resolveCtx: ResolveCtx = {
|
||||
allFilePaths,
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index,
|
||||
resolveCache,
|
||||
configs,
|
||||
};
|
||||
const { addImportEdge, addImportGraphEdge, getResolvedCount } = createImportEdgeHelpers(
|
||||
graph,
|
||||
importMap,
|
||||
);
|
||||
|
||||
// Group by file for progress reporting (users see file count, not import count)
|
||||
const importsByFile = new Map<string, ExtractedImport[]>();
|
||||
for (const imp of extractedImports) {
|
||||
let list = importsByFile.get(imp.filePath);
|
||||
if (!list) {
|
||||
list = [];
|
||||
importsByFile.set(imp.filePath, list);
|
||||
}
|
||||
list.push(imp);
|
||||
}
|
||||
|
||||
const totalFiles = importsByFile.size;
|
||||
let filesProcessed = 0;
|
||||
|
||||
for (const [filePath, fileImports] of importsByFile) {
|
||||
filesProcessed++;
|
||||
if (filesProcessed % 100 === 0) {
|
||||
onProgress?.(filesProcessed, totalFiles);
|
||||
await yieldToEventLoop();
|
||||
}
|
||||
|
||||
for (const imp of fileImports) {
|
||||
totalImportsFound++;
|
||||
|
||||
const provider = getProvider(imp.language);
|
||||
const result = provider.importResolver(imp.rawImportPath, filePath, resolveCtx);
|
||||
applyImportResult(
|
||||
result,
|
||||
filePath,
|
||||
importMap,
|
||||
packageMap,
|
||||
addImportEdge,
|
||||
addImportGraphEdge,
|
||||
imp.namedBindings,
|
||||
namedImportMap,
|
||||
moduleAliasMap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(totalFiles, totalFiles);
|
||||
|
||||
wireImplicitImports(
|
||||
files.map((f) => f.path),
|
||||
importMap,
|
||||
addImportEdge,
|
||||
configs,
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
logger.info(
|
||||
`📊 Import processing (fast path): ${getResolvedCount()}/${totalImportsFound} imports resolved to graph edges`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -22,8 +22,8 @@ export const RESOLVE_CACHE_CAP = 100_000;
|
|||
* - TypeScript/JavaScript: rewrites tsconfig path aliases
|
||||
* - Rust: converts crate::/super::/self:: to relative paths
|
||||
*
|
||||
* Java wildcards and Go package imports are handled separately in processImports
|
||||
* because they resolve to multiple files.
|
||||
* Java wildcards and Go package imports are handled by the scope-resolution
|
||||
* phase because they resolve to multiple files.
|
||||
*/
|
||||
export const resolveImportPath = (
|
||||
currentFile: string,
|
||||
|
|
@ -98,8 +98,8 @@ export const resolveImportPath = (
|
|||
} else if (importPath.startsWith('{') && importPath.endsWith('}')) {
|
||||
// Top-level grouped imports: use {crate::a, crate::b}
|
||||
// Iterate each part and return the first that resolves. This function returns a single
|
||||
// string, so callers that need ALL edges must intercept before reaching here (see the
|
||||
// Rust grouped-import blocks in processImports / processImportsBatch). This fallback
|
||||
// string, so callers that need ALL edges must intercept before reaching here (the
|
||||
// scope-resolution phase handles Rust grouped-import blocks). This fallback
|
||||
// handles any path that reaches resolveImportPath directly.
|
||||
const inner = importPath.slice(1, -1);
|
||||
const parts = inner
|
||||
|
|
@ -150,7 +150,8 @@ export const resolveImportPath = (
|
|||
}
|
||||
|
||||
// ---- Generic package/absolute import resolution (suffix matching) ----
|
||||
// Java wildcards are handled in processImports, not here
|
||||
// Java wildcards are handled by the scope-resolution phase, not here; this
|
||||
// resolver returns null for `.*` so it never produces a single-file match.
|
||||
if (importPath.endsWith('.*')) {
|
||||
return cache(null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/**
|
||||
* Shared utilities for import resolution.
|
||||
* Extracted from import-processor.ts to reduce file size.
|
||||
* Suffix-index helpers for import path resolution.
|
||||
*/
|
||||
|
||||
/** All file extensions to try during resolution */
|
||||
|
|
@ -86,15 +85,6 @@ export interface SuffixIndex {
|
|||
getFilesInDir(dirSuffix: string, extension: string): string[];
|
||||
}
|
||||
|
||||
const FROZEN_EMPTY_ARRAY: string[] = Object.freeze([]) as string[];
|
||||
|
||||
/** Sentinel index that returns no results. Used to release memory after import resolution. */
|
||||
export const EMPTY_INDEX: SuffixIndex = Object.freeze({
|
||||
get: () => undefined,
|
||||
getInsensitive: () => undefined,
|
||||
getFilesInDir: () => FROZEN_EMPTY_ARRAY,
|
||||
});
|
||||
|
||||
export function buildSuffixIndex(normalizedFileList: string[], allFileList: string[]): SuffixIndex {
|
||||
// Map: normalized suffix -> original file path
|
||||
const exactMap = new Map<string, string>();
|
||||
|
|
|
|||
|
|
@ -27,20 +27,13 @@ import type {
|
|||
} from 'gitnexus-shared';
|
||||
import type { LanguageTypeConfig } from './type-extractors/types.js';
|
||||
import type { CallRouter } from './call-routing.js';
|
||||
import type {
|
||||
CallExtractor,
|
||||
DispatchDecision,
|
||||
ImplicitReceiverOverride,
|
||||
ReceiverEnriched,
|
||||
} from './call-types.js';
|
||||
import type { CallExtractor } from './call-types.js';
|
||||
import type { ClassExtractor } from './class-types.js';
|
||||
import type { ExportChecker } from './export-detection.js';
|
||||
import type { FieldExtractor } from './field-extractor.js';
|
||||
import type { HeritageExtractor } from './heritage-types.js';
|
||||
import type { MethodExtractor } from './method-types.js';
|
||||
import type { VariableExtractor } from './variable-types.js';
|
||||
import type { ImportResolverFn } from './import-resolvers/types.js';
|
||||
import type { NamedBindingExtractorFn } from './named-bindings/types.js';
|
||||
import type { SyntaxNode } from './utils/ast-helpers.js';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
|
||||
|
|
@ -53,31 +46,6 @@ export type CaptureMap = Record<string, SyntaxNode | undefined>;
|
|||
// so `core/ingestion/model/resolve.ts` can consume it without importing from
|
||||
// this file (which would pull in the full language-registry dependency graph).
|
||||
|
||||
/**
|
||||
* How a language handles imports — determines wildcard synthesis behavior.
|
||||
*
|
||||
* Import resolution is a graph-traversal policy with multiple distinct strategies,
|
||||
* analogous to MRO for method resolution. Each tag picks a strategy:
|
||||
*
|
||||
* | Tag | Mechanism | Traversal | Languages |
|
||||
* |-----------------------|------------------------------------------------|---------------------|--------------------------------------------|
|
||||
* | `named` | Per-symbol imports | None (use-site) | JS/TS, Java, C#, Rust, PHP, Kotlin, Vue |
|
||||
* | `wildcard-transitive` | Textual paste, symbols chain through files | BFS closure | C, C++ (future: Obj-C, Fortran, Nim) |
|
||||
* | `wildcard-leaf` | Whole public API, single hop | None (direct only) | Go, Ruby, Swift, Dart |
|
||||
* | `namespace` | Qualified handle; symbols resolved at call site| None at import | Python |
|
||||
* | `explicit-reexport` | Opt-in per-symbol re-export (SCAFFOLD) | Topological DAG | (future: TS `export *`, Rust `pub use`) |
|
||||
*
|
||||
* The `explicit-reexport` tag is a compile-time scaffold; no provider claims it yet.
|
||||
* It falls through to `wildcard-leaf` behavior in synthesis so today's TS/Rust
|
||||
* handling is unchanged. A future PR will implement the DAG walk for `export *`.
|
||||
*/
|
||||
export type ImportSemantics =
|
||||
| 'named'
|
||||
| 'wildcard-transitive'
|
||||
| 'wildcard-leaf'
|
||||
| 'namespace'
|
||||
| 'explicit-reexport';
|
||||
|
||||
/** Configuration for AST-based framework detection patterns. */
|
||||
export interface AstFrameworkPatternConfig {
|
||||
framework: string;
|
||||
|
|
@ -161,31 +129,10 @@ interface LanguageProviderConfig {
|
|||
/** Call routing for languages that express imports/heritage as calls (e.g., Ruby).
|
||||
* Default: no routing (all calls are normal call expressions). */
|
||||
readonly callRouter?: CallRouter;
|
||||
/** Named binding extraction from import statements.
|
||||
* Default: undefined (language uses wildcard/whole-module imports). */
|
||||
readonly namedBindingExtractor?: NamedBindingExtractorFn;
|
||||
/** How this language handles imports. See `ImportSemantics` for the full taxonomy.
|
||||
* - 'named': per-symbol imports (JS/TS, Java, C#, Rust, PHP, Kotlin)
|
||||
* - 'wildcard-transitive': textual-include closure; imports chain through files (C, C++)
|
||||
* - 'wildcard-leaf': whole-module single-hop imports; no transitive chaining (Go, Ruby, Swift, Dart)
|
||||
* - 'namespace': qualified namespace imports, needs moduleAliasMap (Python)
|
||||
* - 'explicit-reexport': opt-in per-symbol re-export (scaffold; no provider uses yet)
|
||||
* Default: 'named'. */
|
||||
readonly importSemantics?: ImportSemantics;
|
||||
/** Language-specific transformation of raw import path text before resolution.
|
||||
* Called after sanitization. E.g., Kotlin appends wildcard suffixes.
|
||||
* Default: undefined (no preprocessing). */
|
||||
readonly importPathPreprocessor?: (cleaned: string, importNode: SyntaxNode) => string;
|
||||
/** Wire implicit inter-file imports for languages where all files in a module
|
||||
* see each other (e.g., Swift targets, C header inclusion units).
|
||||
* Called with only THIS language's files (pre-grouped by the processor).
|
||||
* Default: undefined (no implicit imports). */
|
||||
readonly implicitImportWirer?: (
|
||||
languageFiles: string[],
|
||||
importMap: ReadonlyMap<string, ReadonlySet<string>>,
|
||||
addImportEdge: (src: string, target: string) => void,
|
||||
projectConfig: unknown,
|
||||
) => void;
|
||||
|
||||
// ── Enclosing owner resolution ─────────────────────────────────
|
||||
/** Resolve a container node during enclosing-owner tree walks.
|
||||
|
|
@ -248,13 +195,7 @@ interface LanguageProviderConfig {
|
|||
* Default: undefined (standard label assignment). */
|
||||
readonly labelOverride?: (functionNode: SyntaxNode, defaultLabel: NodeLabel) => NodeLabel | null;
|
||||
|
||||
// ── Heritage & MRO ────────────────────────────────────────────────
|
||||
/** Default edge type when parent symbol is ambiguous (interface vs class).
|
||||
* Default: 'EXTENDS'. */
|
||||
readonly heritageDefaultEdge?: 'EXTENDS' | 'IMPLEMENTS';
|
||||
/** Regex to detect interface names by convention (e.g., /^I[A-Z]/ for C#/Java).
|
||||
* When matched, IMPLEMENTS edge is used instead of heritageDefaultEdge. */
|
||||
readonly interfaceNamePattern?: RegExp;
|
||||
// ── MRO ───────────────────────────────────────────────────────────
|
||||
/** MRO strategy for multiple inheritance resolution.
|
||||
* Default: 'first-wins'. */
|
||||
readonly mroStrategy?: MroStrategy;
|
||||
|
|
@ -282,13 +223,6 @@ interface LanguageProviderConfig {
|
|||
* Uses the same provider-driven strategy pattern as method/field extraction so
|
||||
* namespace/package/module rules stay language-specific. */
|
||||
readonly classExtractor?: ClassExtractor;
|
||||
/** Heritage extractor for extracting extends/implements/trait-impl relationships
|
||||
* from tree-sitter @heritage.* captures and call-based heritage (e.g., Ruby
|
||||
* include/extend/prepend). Produced by createHeritageExtractor() — pass a
|
||||
* SupportedLanguages value for default behaviour or a full
|
||||
* HeritageExtractionConfig for languages with custom hooks (Go, Ruby).
|
||||
* All tree-sitter providers MUST supply this. */
|
||||
readonly heritageExtractor?: HeritageExtractor;
|
||||
/** Extract a semantic description for a definition node (e.g., PHP Eloquent
|
||||
* property arrays, relation method descriptions).
|
||||
* Default: undefined (no description extraction). */
|
||||
|
|
@ -302,69 +236,6 @@ interface LanguageProviderConfig {
|
|||
* Default: undefined (no route files). */
|
||||
readonly isRouteFile?: (filePath: string) => boolean;
|
||||
|
||||
// ── Call-resolution DAG hooks ─────────────────────────────────────
|
||||
/**
|
||||
* DAG stage 3 hook: synthesize an implicit receiver when the call site omits one.
|
||||
*
|
||||
* Runs after shared inference (TypeEnv → constructor-map → class-as-receiver →
|
||||
* mixed-chain). Return an `ImplicitReceiverOverride` to overlay all fields onto
|
||||
* `ReceiverEnriched`; return null to keep current state and proceed to stage 4.
|
||||
*
|
||||
* Constraints: MUST return null when an explicit receiver is already set, at
|
||||
* top-level scope, or for built-in methods. Do not mutate input params.
|
||||
* `hint` is opaque to shared stages; consumed by this language's `selectDispatch`.
|
||||
*
|
||||
* Ruby example: bare `serialize` in `Account#call_serialize` →
|
||||
* `{ callForm: 'member', receiverName: 'self', receiverTypeName: 'Account',
|
||||
* receiverSource: 'implicit-self', hint: 'instance' }`
|
||||
*
|
||||
* @see call-types.ts § ImplicitReceiverOverride
|
||||
* @see selectDispatch (stage 4, reads the hint)
|
||||
*
|
||||
* Default: undefined (no implicit-receiver inference).
|
||||
*/
|
||||
readonly inferImplicitReceiver?: (params: {
|
||||
readonly calledName: string;
|
||||
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
||||
readonly receiverName: string | undefined;
|
||||
readonly receiverTypeName: string | undefined;
|
||||
readonly callNode: SyntaxNode;
|
||||
readonly filePath: string;
|
||||
}) => ImplicitReceiverOverride | null;
|
||||
|
||||
/**
|
||||
* DAG stage 4 hook: decide dispatch strategy (primary path, fallback, MRO view).
|
||||
*
|
||||
* Runs after stage 3. Return a `DispatchDecision` to override shared defaults;
|
||||
* return null to use `defaultDispatchDecision` (constructor→`'constructor'`,
|
||||
* member→`'owner-scoped'`, free→`'free'`). Most languages return null.
|
||||
*
|
||||
* The hook is responsible for its own gating. `ancestryView` only affects
|
||||
* `'ruby-mixin'` strategy. Singleton-ancestry miss NEVER falls through to
|
||||
* file-scoped fallback in stage 5 (enforced in resolveCallTarget).
|
||||
*
|
||||
* Ruby examples:
|
||||
* - `receiverSource='implicit-self', hint='instance'` →
|
||||
* `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'instance'}`
|
||||
* - `receiverSource='class-as-receiver'` →
|
||||
* `{primary: 'owner-scoped', ancestryView: 'singleton'}` (miss null-routes)
|
||||
* - `receiverSource='implicit-self', hint='singleton'` →
|
||||
* `{primary: 'owner-scoped', fallback: 'free-arity-narrowed', ancestryView: 'singleton'}`
|
||||
*
|
||||
* @see call-types.ts § DispatchDecision
|
||||
* @see call-processor.ts § defaultDispatchDecision, resolveCallTarget
|
||||
*
|
||||
* Default: undefined (use `defaultDispatchDecision`).
|
||||
*/
|
||||
readonly selectDispatch?: (params: {
|
||||
readonly calledName: string;
|
||||
readonly callForm: 'free' | 'member' | 'constructor' | undefined;
|
||||
readonly receiverName: string | undefined;
|
||||
readonly receiverTypeName: string | undefined;
|
||||
readonly receiverSource: ReceiverEnriched['receiverSource'];
|
||||
readonly hint: string | undefined;
|
||||
}) => DispatchDecision | null;
|
||||
|
||||
// ── Noise filtering ────────────────────────────────────────────────
|
||||
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
||||
* Default: undefined (no language-specific filtering). */
|
||||
|
|
@ -400,9 +271,8 @@ interface LanguageProviderConfig {
|
|||
* routing (scope / declaration / import / type-binding / reference)
|
||||
* lands on coherent records.
|
||||
*
|
||||
* Required for any provider participating in scope-based resolution.
|
||||
* Providers that have not yet migrated continue to run through the
|
||||
* legacy DAG path (feature-flagged per `REGISTRY_PRIMARY_<LANG>`).
|
||||
* Required for any provider participating in scope-based resolution
|
||||
* (the sole resolution path).
|
||||
*
|
||||
* **Sync return.** Tree-sitter query execution and COBOL's regex
|
||||
* tagger are both synchronous; no current or foreseeable provider
|
||||
|
|
@ -410,7 +280,7 @@ interface LanguageProviderConfig {
|
|||
* `parse-worker.ts` (#920) invoke it inline in its already-sync
|
||||
* per-file loop without cascading `async` through the batch pipeline.
|
||||
*
|
||||
* Default: undefined (language continues to use legacy DAG).
|
||||
* Default: undefined (no scope-based captures emitted for this language).
|
||||
*/
|
||||
readonly emitScopeCaptures?: (
|
||||
sourceText: string,
|
||||
|
|
@ -622,23 +492,15 @@ interface LanguageProviderConfig {
|
|||
}
|
||||
|
||||
/** Runtime type — same as LanguageProviderConfig but with defaults guaranteed present. */
|
||||
export interface LanguageProvider extends Omit<
|
||||
LanguageProviderConfig,
|
||||
'importSemantics' | 'heritageDefaultEdge' | 'mroStrategy'
|
||||
> {
|
||||
readonly importSemantics: ImportSemantics;
|
||||
readonly heritageDefaultEdge: 'EXTENDS' | 'IMPLEMENTS';
|
||||
export interface LanguageProvider extends Omit<LanguageProviderConfig, 'mroStrategy'> {
|
||||
readonly mroStrategy: MroStrategy;
|
||||
/** Check if a name is a built-in/stdlib function that should be filtered from the call graph. */
|
||||
readonly isBuiltInName: (name: string) => boolean;
|
||||
}
|
||||
|
||||
const DEFAULTS: Pick<LanguageProvider, 'importSemantics' | 'heritageDefaultEdge' | 'mroStrategy'> =
|
||||
{
|
||||
importSemantics: 'named',
|
||||
heritageDefaultEdge: 'EXTENDS',
|
||||
mroStrategy: 'first-wins',
|
||||
};
|
||||
const DEFAULTS: Pick<LanguageProvider, 'mroStrategy'> = {
|
||||
mroStrategy: 'first-wins',
|
||||
};
|
||||
|
||||
/** Define a language provider — required fields must be supplied, optional fields get sensible defaults. */
|
||||
export function defineLanguage(config: LanguageProviderConfig): LanguageProvider {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { cVariableConfig, cppVariableConfig } from '../variable-extractors/configs/c-cpp.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import { stripUeMacros } from '../cpp-ue-preprocessor.js';
|
||||
import {
|
||||
emitCScopeCaptures,
|
||||
|
|
@ -383,7 +382,6 @@ export const cProvider = defineLanguage({
|
|||
typeConfig: cCppConfig,
|
||||
exportChecker: cCppExportChecker,
|
||||
importResolver: createImportResolver(cImportConfig),
|
||||
importSemantics: 'wildcard-transitive',
|
||||
callExtractor: createCallExtractor(cCallConfig),
|
||||
fieldExtractor: createFieldExtractor(cFieldConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
|
|
@ -392,7 +390,6 @@ export const cProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(cVariableConfig),
|
||||
classExtractor: cClassExtractor,
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.C),
|
||||
labelOverride: cppLabelOverride,
|
||||
builtInNames: C_BUILT_INS,
|
||||
|
||||
|
|
@ -453,7 +450,6 @@ export const cppProvider = defineLanguage({
|
|||
typeConfig: cCppConfig,
|
||||
exportChecker: cCppExportChecker,
|
||||
importResolver: createImportResolver(cppImportConfig),
|
||||
importSemantics: 'wildcard-transitive',
|
||||
mroStrategy: 'leftmost-base',
|
||||
callExtractor: createCallExtractor(cppCallConfig),
|
||||
fieldExtractor: createFieldExtractor(cppFieldConfig),
|
||||
|
|
@ -463,7 +459,6 @@ export const cppProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(cppVariableConfig),
|
||||
classExtractor: cppClassExtractor,
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.CPlusPlus),
|
||||
labelOverride: cppLabelOverride,
|
||||
builtInNames: C_BUILT_INS,
|
||||
extractTemplateConstraints: extractCppTemplateConstraintsForProvider,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { typeConfig as csharpConfig } from '../type-extractors/csharp.js';
|
|||
import { csharpExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { csharpImportConfig } from '../import-resolvers/configs/csharp.js';
|
||||
import { extractCSharpNamedBindings } from '../named-bindings/csharp.js';
|
||||
import { CSHARP_QUERIES } from '../tree-sitter-queries.js';
|
||||
import type { AstFrameworkPatternConfig } from '../language-provider.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
|
|
@ -25,7 +24,6 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
|
|||
import { csharpMethodConfig } from '../method-extractors/configs/csharp.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { csharpVariableConfig } from '../variable-extractors/configs/csharp.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
emitCsharpScopeCaptures,
|
||||
interpretCsharpImport,
|
||||
|
|
@ -189,15 +187,12 @@ export const csharpProvider = defineLanguage({
|
|||
typeConfig: csharpConfig,
|
||||
exportChecker: csharpExportChecker,
|
||||
importResolver: createImportResolver(csharpImportConfig),
|
||||
namedBindingExtractor: extractCSharpNamedBindings,
|
||||
interfaceNamePattern: /^I[A-Z]/,
|
||||
mroStrategy: 'implements-split',
|
||||
callExtractor: createCallExtractor(csharpCallConfig),
|
||||
fieldExtractor: createFieldExtractor(csharpFieldConfig),
|
||||
methodExtractor: createMethodExtractor(csharpMethodConfig),
|
||||
variableExtractor: createVariableExtractor(csharpVariableConfig),
|
||||
classExtractor: createClassExtractor(csharpClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.CSharp),
|
||||
builtInNames: BUILT_INS,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
|
|
|
|||
|
|
@ -271,11 +271,12 @@ export function emitCsharpScopeCaptures(
|
|||
* Synthesize `@reference.inherits` captures from C# base lists so the
|
||||
* registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
|
||||
* (mirrors C++ `emitCppInheritanceCaptures`). Without this, C# inheritance
|
||||
* edges came only from the legacy `@heritage.*` path, which is dropped for
|
||||
* registry-primary languages in the worker pipeline (issue #1951).
|
||||
* edges came only from the legacy heritage-capture leg (removed in #942),
|
||||
* which is dropped for registry-primary languages in the worker pipeline
|
||||
* (issue #1951).
|
||||
*
|
||||
* Scope covers every `base_list`-bearing declaration the legacy `@heritage`
|
||||
* leg matches: `class_declaration`, `interface_declaration`,
|
||||
* Scope covers every `base_list`-bearing declaration the legacy heritage
|
||||
* leg matched: `class_declaration`, `interface_declaration`,
|
||||
* `record_declaration`, and `struct_declaration`. Records and structs were
|
||||
* dropped before (#1951): a `record R(...) : Base(args), IFoo` or
|
||||
* `struct S : IFoo, ns.IBar` produced no registry-primary inheritance edge
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* Dart Language Provider
|
||||
*
|
||||
* Dart traits:
|
||||
* - importSemantics: 'wildcard-leaf' (Dart imports bring everything public into scope)
|
||||
* - exportChecker: public if no leading underscore
|
||||
* - Dart SDK imports (dart:*) and external packages are skipped
|
||||
* - enclosingFunctionFinder: Dart's tree-sitter grammar places function_body
|
||||
|
|
@ -31,7 +30,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { dartVariableConfig } from '../variable-extractors/configs/dart.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { dartCallConfig } from '../call-extractors/configs/dart.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
emitDartScopeCaptures,
|
||||
interpretDartImport,
|
||||
|
|
@ -120,13 +118,11 @@ export const dartProvider = defineLanguage({
|
|||
typeConfig: dartConfig,
|
||||
exportChecker: dartExportChecker,
|
||||
importResolver: createImportResolver(dartImportConfig),
|
||||
importSemantics: 'wildcard-leaf',
|
||||
callExtractor: createCallExtractor(dartCallConfig),
|
||||
fieldExtractor: createFieldExtractor(dartFieldConfig),
|
||||
methodExtractor: createMethodExtractor(dartMethodConfig),
|
||||
variableExtractor: createVariableExtractor(dartVariableConfig),
|
||||
classExtractor: createClassExtractor(dartClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Dart),
|
||||
enclosingFunctionFinder: dartEnclosingFunctionFinder,
|
||||
builtInNames: DART_BUILT_INS,
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import { getDartParser, getDartScopeQuery } from './query.js';
|
|||
import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { DART_HERITAGE_PREFIX } from './interpret.js';
|
||||
import { encodeMarker } from '../../utils/heritage-marker.js';
|
||||
import { DART_BUILT_INS } from './built-ins.js';
|
||||
|
||||
const FUNCTION_DECL_TAGS = [
|
||||
|
|
@ -491,7 +491,7 @@ function emitHeritageMarkers(
|
|||
for (let i = 0; i < container.namedChildCount; i++) {
|
||||
const c = container.namedChild(i);
|
||||
if (c === null || c.type !== 'type_identifier') continue;
|
||||
const payload = `${DART_HERITAGE_PREFIX}${kind}:${c.text}:${className}`;
|
||||
const payload = encodeMarker('heritage', [kind, c.text, className]);
|
||||
out.push({ '@import.heritage': syntheticCapture('@import.heritage', c, payload) });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* - `interpretDartImport` — `@import.source` → a whole-library
|
||||
* `ParsedImport` (Dart `import`/`export` bring every public top-level
|
||||
* symbol of the target into scope: `importSemantics: 'wildcard-leaf'`).
|
||||
* symbol of the target into scope — whole-library / wildcard-leaf semantics).
|
||||
* `@import.heritage` markers (synthesized by `captures.ts` for
|
||||
* `implements`/`with` clauses) become side-effect imports carrying a
|
||||
* `__heritage__:` payload that `emitDartHeritageEdges` consumes; they
|
||||
|
|
@ -15,10 +15,13 @@
|
|||
*/
|
||||
|
||||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
|
||||
import { HERITAGE_MARKER_PREFIX } from '../../utils/heritage-marker.js';
|
||||
|
||||
/** Marker prefix carried on a side-effect `ParsedImport.targetRaw` for
|
||||
* `implements`/`with` heritage, consumed by `emitDartHeritageEdges`. */
|
||||
export const DART_HERITAGE_PREFIX = '__heritage__:';
|
||||
* `implements`/`with` heritage, consumed by `emitDartHeritageEdges`. Aliased to
|
||||
* the shared codec prefix (#1994) so the Dart wire prefix has a single source of
|
||||
* truth and cannot desync from `encodeMarker`/`decodeMarker`. */
|
||||
export const DART_HERITAGE_PREFIX = HERITAGE_MARKER_PREFIX;
|
||||
|
||||
function stripQuotes(s: string): string {
|
||||
return s.replace(/^['"]|['"]$/g, '');
|
||||
|
|
|
|||
|
|
@ -36,12 +36,8 @@ import type { KnowledgeGraph } from '../../../graph/types.js';
|
|||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { generateId } from '../../../../lib/utils.js';
|
||||
import { dartProvider } from '../dart.js';
|
||||
import {
|
||||
dartArityCompatibility,
|
||||
dartMergeBindings,
|
||||
resolveDartImportTarget,
|
||||
DART_HERITAGE_PREFIX,
|
||||
} from './index.js';
|
||||
import { dartArityCompatibility, dartMergeBindings, resolveDartImportTarget } from './index.js';
|
||||
import { decodeMarker } from '../../utils/heritage-marker.js';
|
||||
import { expandDartWildcardNames } from './expand-wildcards.js';
|
||||
|
||||
interface ClassDefRef {
|
||||
|
|
@ -109,8 +105,10 @@ function emitDartHeritageEdges(
|
|||
for (const parsed of parsedFiles) {
|
||||
for (const imp of parsed.parsedImports) {
|
||||
const raw = imp.targetRaw;
|
||||
if (typeof raw !== 'string' || !raw.startsWith(DART_HERITAGE_PREFIX)) continue;
|
||||
const parts = raw.slice(DART_HERITAGE_PREFIX.length).split(':');
|
||||
if (typeof raw !== 'string') continue;
|
||||
const decoded = decodeMarker(raw);
|
||||
if (decoded?.kind !== 'heritage') continue;
|
||||
const parts = decoded.fields;
|
||||
if (parts.length < 3) continue;
|
||||
const [kind, baseName, childName] = parts;
|
||||
const childId = pickClassByName(childName!, parsed.filePath, defsByName);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
* LanguageProvider, following the Strategy pattern used by the pipeline.
|
||||
*
|
||||
* Key Go traits:
|
||||
* - importSemantics: 'wildcard-leaf' (Go imports entire packages)
|
||||
* - callRouter: present (Go method calls may need routing)
|
||||
*/
|
||||
|
||||
|
|
@ -27,8 +26,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { goVariableConfig } from '../variable-extractors/configs/go.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { goCallConfig } from '../call-extractors/configs/go.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import { goHeritageConfig } from '../heritage-extractors/configs/go.js';
|
||||
import {
|
||||
emitGoScopeCaptures,
|
||||
goArityCompatibility,
|
||||
|
|
@ -135,13 +132,11 @@ export const goProvider = defineLanguage({
|
|||
typeConfig: goConfig,
|
||||
exportChecker: goExportChecker,
|
||||
importResolver: createImportResolver(goImportConfig),
|
||||
importSemantics: 'wildcard-leaf',
|
||||
callExtractor: createCallExtractor(goCallConfig),
|
||||
fieldExtractor: createFieldExtractor(goFieldConfig),
|
||||
methodExtractor: createMethodExtractor(goMethodConfig),
|
||||
variableExtractor: createVariableExtractor(goVariableConfig),
|
||||
classExtractor: createClassExtractor(goClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(goHeritageConfig),
|
||||
builtInNames: GO_BUILT_INS,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
|
||||
|
|
|
|||
|
|
@ -183,9 +183,9 @@ export function emitGoScopeCaptures(
|
|||
* Synthesize `@reference.inherits` captures for Go struct embedding so the
|
||||
* registry-primary scope-resolution path emits inheritance edges (mirrors C#
|
||||
* `synthesizeCsharpInheritanceReferences` / C++ `emitCppInheritanceCaptures`).
|
||||
* Without this, Go embedding edges came only from the legacy `@heritage.*`
|
||||
* path, which is dropped for registry-primary languages in the worker pipeline
|
||||
* (issue #1951).
|
||||
* Without this, Go embedding edges came only from the legacy heritage-capture
|
||||
* leg (removed in #942), which is dropped for registry-primary languages in the
|
||||
* worker pipeline (issue #1951).
|
||||
*
|
||||
* Scope EXACTLY matches the legacy Go heritage query + its `shouldSkipExtends`
|
||||
* hook (`heritage-extractors/configs/go.ts`), whose supertype alternation is
|
||||
|
|
@ -204,7 +204,7 @@ export function emitGoScopeCaptures(
|
|||
*
|
||||
* The base shapes covered (issue #1951 — these were previously DROPPED by the
|
||||
* registry-primary synth, so production silently omitted their edges even though
|
||||
* the legacy `@heritage` leg, config-driven since #1940, captured them):
|
||||
* the legacy heritage leg, config-driven since #1940, captured them):
|
||||
* - bare `type_identifier` (`Base`) → the node itself
|
||||
* - `qualified_type` (`pkg.Base`) → `name:` tail
|
||||
* - `generic_type` (`Box[T]`) → `type:` base
|
||||
|
|
@ -276,7 +276,7 @@ function emitGoEmbedInheritance(baseNode: SyntaxNode | null, out: CaptureMatch[]
|
|||
|
||||
/**
|
||||
* Reduce a Go embed base node to its trailing bare `type_identifier`, matching
|
||||
* the node shapes the legacy `@heritage` query accepts (`goHeritageShapes`) and
|
||||
* the node shapes the legacy heritage query accepted (`goHeritageShapes`) and
|
||||
* the reduction `normalizeSupertypeName` performs (verified by real-parse to
|
||||
* yield an identical `.text` for each shape):
|
||||
* - `type_identifier` → the node itself (`Base`)
|
||||
|
|
|
|||
|
|
@ -68,13 +68,3 @@ export function getProviderForFile(filePath: string): LanguageProvider | null {
|
|||
const basename = filePath.slice(filePath.lastIndexOf('/') + 1);
|
||||
return extensionMap.get(ext) ?? extensionMap.get(basename) ?? null;
|
||||
}
|
||||
|
||||
/** Pre-computed list of providers that have implicit import wiring (e.g., Swift).
|
||||
* Built once at module load — avoids iterating all 13 providers per call. */
|
||||
export const providersWithImplicitWiring = Object.values(providers).filter(
|
||||
(
|
||||
p,
|
||||
): p is LanguageProvider & {
|
||||
implicitImportWirer: NonNullable<LanguageProvider['implicitImportWirer']>;
|
||||
} => p.implicitImportWirer != null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|||
import { javaExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
||||
import { extractJavaNamedBindings } from '../named-bindings/java.js';
|
||||
import { JAVA_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { javaCallConfig } from '../call-extractors/configs/jvm.js';
|
||||
|
|
@ -26,7 +25,6 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
|
|||
import { javaMethodConfig } from '../method-extractors/configs/jvm.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { javaVariableConfig } from '../variable-extractors/configs/jvm.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import type { SymbolDefinition } from 'gitnexus-shared';
|
||||
import {
|
||||
emitJavaScopeCaptures,
|
||||
|
|
@ -110,15 +108,12 @@ export const javaProvider = defineLanguage({
|
|||
typeConfig: javaTypeConfig,
|
||||
exportChecker: javaExportChecker,
|
||||
importResolver: createImportResolver(javaImportConfig),
|
||||
namedBindingExtractor: extractJavaNamedBindings,
|
||||
interfaceNamePattern: /^I[A-Z]/,
|
||||
mroStrategy: 'implements-split',
|
||||
callExtractor: createCallExtractor(javaCallConfig),
|
||||
fieldExtractor: createFieldExtractor(javaConfig),
|
||||
methodExtractor: createMethodExtractor(javaMethodConfig),
|
||||
variableExtractor: createVariableExtractor(javaVariableConfig),
|
||||
classExtractor: createClassExtractor(javaClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Java),
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
||||
emitScopeCaptures: emitJavaScopeCaptures,
|
||||
|
|
|
|||
|
|
@ -223,8 +223,8 @@ export function emitJavaScopeCaptures(
|
|||
* Synthesize `@reference.inherits` captures from Java class heritage so the
|
||||
* registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
|
||||
* (mirrors C++ `emitCppInheritanceCaptures`). Without this, Java inheritance
|
||||
* edges came only from the legacy `@heritage.*` path, which is dropped for
|
||||
* registry-primary languages in the worker pipeline (issue #1951).
|
||||
* edges came only from the legacy heritage-capture leg (removed in #942), which
|
||||
* is dropped for registry-primary languages in the worker pipeline (issue #1951).
|
||||
*
|
||||
* Scope covers `class_declaration` (`superclass` extends + `interfaces`
|
||||
* implements clauses) AND `interface_declaration` (`extends_interfaces` →
|
||||
|
|
@ -235,19 +235,19 @@ export function emitJavaScopeCaptures(
|
|||
* edge while the legacy leg emitted it — the exact =0/=N parity break #1951
|
||||
* targets. Enum/record heritage stays unemitted (no legacy arm). Generic
|
||||
* bases (`extends Box<T>`, `implements IFoo<T>`) ARE emitted here: the legacy
|
||||
* `@heritage` query was widened to capture the inner `type_identifier` of a
|
||||
* heritage query was widened to capture the inner `type_identifier` of a
|
||||
* `generic_type` (tree-sitter-queries.ts), so both paths now agree on SIMPLE
|
||||
* (unqualified) generic bases — the more-correct behavior, consistent with
|
||||
* C#/Rust (#1951). Qualified bases (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are
|
||||
* ALSO now at parity (#1956 tri-review U2): the synth resolves them by their
|
||||
* `scoped_type_identifier` tail, and the legacy `@heritage` query was widened
|
||||
* `scoped_type_identifier` tail, and the legacy heritage query was widened
|
||||
* with matching `scoped_type_identifier` arms (plain + generic-wrapped). The
|
||||
* EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved target's
|
||||
* symbol kind (`preEmitInheritanceEdges`): a superclass resolves to a class
|
||||
* (EXTENDS), an implemented interface resolves to an interface (IMPLEMENTS).
|
||||
* An `interface IA extends IB` base resolves to an Interface too, so it is
|
||||
* emitted as IMPLEMENTS — matching the legacy `interface_declaration` arm,
|
||||
* which tags the bases `@heritage.impl` (`kind: 'implements'`) and likewise
|
||||
* which tagged the bases as implements (`kind: 'implements'`) and likewise
|
||||
* resolves them as interfaces. The synth therefore does not need to know the
|
||||
* declaration's own kind; it only emits inherits sites and lets the resolved
|
||||
* target decide the edge type.
|
||||
|
|
@ -279,7 +279,7 @@ function synthesizeJavaInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
|
|||
// whose bases reuse `javaBaseLookupNameNode` (handles type_identifier /
|
||||
// generic_type / scoped_type_identifier). These resolve to Interface
|
||||
// targets, so `preEmitInheritanceEdges` emits them as IMPLEMENTS, at
|
||||
// parity with the legacy `interface_declaration` @heritage.impl arm.
|
||||
// parity with the legacy `interface_declaration` implements arm.
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const extendsInterfaces = node.namedChild(i);
|
||||
if (extendsInterfaces === null || extendsInterfaces.type !== 'extends_interfaces') continue;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,8 @@
|
|||
* Java `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
||||
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
||||
*
|
||||
* ## Registry-primary parity status
|
||||
*
|
||||
* Java is in `MIGRATED_LANGUAGES` — the scope-resolution registry is
|
||||
* the primary call-resolution path. Parity: 178/178 (100%).
|
||||
*
|
||||
* **CI visibility:** The parity CI workflow (`ci-scope-parity.yml`)
|
||||
* runs Java tests in both `REGISTRY_PRIMARY_JAVA=0` and `=1` modes
|
||||
* automatically.
|
||||
* Java resolves via the scope-resolution registry — the sole
|
||||
* call-resolution path.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, TypeRef } from 'gitnexus-shared';
|
||||
|
|
|
|||
|
|
@ -639,19 +639,19 @@ function synthesizeConstructorFieldBindings(root: SyntaxNode, out: CaptureMatch[
|
|||
* Synthesize `@reference.inherits` captures from JavaScript class heritage so
|
||||
* the registry-primary scope-resolution path emits EXTENDS edges (mirrors C#
|
||||
* `synthesizeCsharpInheritanceReferences` / C++ `emitCppInheritanceCaptures`).
|
||||
* Without this, JS inheritance edges came only from the legacy `@heritage.*`
|
||||
* path, which the worker pipeline drops for registry-primary languages,
|
||||
* yielding 0 inheritance edges in worker mode (issue #1951).
|
||||
* Without this, JS inheritance edges came only from the legacy heritage-capture
|
||||
* leg (removed in #942), which the worker pipeline drops for registry-primary
|
||||
* languages, yielding 0 inheritance edges in worker mode (issue #1951).
|
||||
*
|
||||
* Scope is intentionally limited to a `class_declaration`'s `class_heritage`
|
||||
* base, matching the legacy JavaScript `@heritage` query's class scope and its
|
||||
* base, matching the legacy JavaScript heritage query's class scope and its
|
||||
* supertype shape descriptor (`javascriptHeritageShapes`:
|
||||
* `['identifier', 'member_expression']`). JavaScript classes have a single
|
||||
* `extends` base and no `implements`, so every emission is an EXTENDS (decided
|
||||
* downstream from the resolved target's symbol kind in
|
||||
* `preEmitInheritanceEdges`).
|
||||
*
|
||||
* Bases handled (at parity with the legacy `@heritage` leg, #1951):
|
||||
* Bases handled (at parity with the legacy heritage leg, #1951):
|
||||
* - `(identifier)` base (`extends Base`) — bare simple name.
|
||||
* - `(member_expression)` base (`extends ns.Base`, `extends a.b.Base`) —
|
||||
* qualified; reduced to its trailing `property_identifier` (`Base`) so the
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { kotlinTypeConfig } from '../type-extractors/jvm.js';
|
|||
import { kotlinExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { kotlinImportConfig } from '../import-resolvers/configs/jvm.js';
|
||||
import { extractKotlinNamedBindings } from '../named-bindings/kotlin.js';
|
||||
import { appendKotlinWildcard } from '../import-resolvers/jvm.js';
|
||||
import { KOTLIN_QUERIES } from '../tree-sitter-queries.js';
|
||||
import type { AstFrameworkPatternConfig } from '../language-provider.js';
|
||||
|
|
@ -28,7 +27,6 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
|
|||
import { kotlinMethodConfig } from '../method-extractors/configs/jvm.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { kotlinVariableConfig } from '../variable-extractors/configs/jvm.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
emitKotlinScopeCaptures,
|
||||
interpretKotlinImport,
|
||||
|
|
@ -161,7 +159,6 @@ export const kotlinProvider = defineLanguage({
|
|||
typeConfig: kotlinTypeConfig,
|
||||
exportChecker: kotlinExportChecker,
|
||||
importResolver: createImportResolver(kotlinImportConfig),
|
||||
namedBindingExtractor: extractKotlinNamedBindings,
|
||||
importPathPreprocessor: appendKotlinWildcard,
|
||||
mroStrategy: 'implements-split',
|
||||
callExtractor: createCallExtractor(kotlinCallConfig),
|
||||
|
|
@ -169,7 +166,6 @@ export const kotlinProvider = defineLanguage({
|
|||
methodExtractor: createMethodExtractor(kotlinMethodConfig),
|
||||
variableExtractor: createVariableExtractor(kotlinVariableConfig),
|
||||
classExtractor: createClassExtractor(kotlinClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Kotlin),
|
||||
builtInNames: BUILT_INS,
|
||||
labelOverride: (functionNode, defaultLabel) => {
|
||||
if (defaultLabel !== 'Function') return defaultLabel;
|
||||
|
|
|
|||
|
|
@ -195,10 +195,11 @@ export function emitKotlinScopeCaptures(
|
|||
* delegation specifiers so the registry-primary scope-resolution path emits
|
||||
* EXTENDS / IMPLEMENTS edges (mirrors C# `synthesizeCsharpInheritanceReferences`
|
||||
* and C++ `emitCppInheritanceCaptures`). Without this, Kotlin inheritance edges
|
||||
* came only from the legacy `@heritage.*` path, which the worker pipeline drops
|
||||
* for registry-primary languages → 0 inheritance edges in worker mode (#1951).
|
||||
* came only from the legacy heritage-capture leg (removed in #942), which the
|
||||
* worker pipeline drops for registry-primary languages → 0 inheritance edges in
|
||||
* worker mode (#1951).
|
||||
*
|
||||
* Scope mirrors the legacy KOTLIN_QUERIES `@heritage.extends` patterns exactly
|
||||
* Scope mirrors the legacy KOTLIN_QUERIES heritage patterns exactly
|
||||
* (the config-driven `kotlinHeritageShapes`: `user_type`,
|
||||
* `constructor_invocation`, `explicit_delegation`). Each `delegation_specifier`
|
||||
* child of a `class_declaration`, in one of three forms —
|
||||
|
|
@ -256,7 +257,7 @@ function synthesizeKotlinInheritanceReferences(rootNode: SyntaxNode): CaptureMat
|
|||
* The bare simple-name `type_identifier` of a `user_type`. Strips generic
|
||||
* type arguments (`Base<T>` → `Base`) and qualifier tails (`pkg.Base` → `Base`)
|
||||
* by taking the LAST direct `type_identifier` child, matching the legacy
|
||||
* `(user_type (type_identifier) @heritage.extends)` capture and V1's
|
||||
* heritage capture of a `user_type`'s `type_identifier` and V1's
|
||||
* simple-name `findClassBindingInScope` contract.
|
||||
*/
|
||||
function kotlinUserTypeNameNode(userType: SyntaxNode): SyntaxNode | null {
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@ import { isKotlinStaticOnly } from './owners.js';
|
|||
/**
|
||||
* Kotlin scope resolver for RFC #909 Ring 3.
|
||||
*
|
||||
* **Migration status:** Kotlin is in `MIGRATED_LANGUAGES`. Default
|
||||
* production resolution flows through the scope-resolution pipeline;
|
||||
* the legacy DAG is consulted only when the per-language env var
|
||||
* (`REGISTRY_PRIMARY_KOTLIN=0`) explicitly forces the legacy parity
|
||||
* run for CI comparison.
|
||||
* Kotlin resolves via the scope-resolution registry — production
|
||||
* resolution flows through the scope-resolution pipeline as the sole
|
||||
* call-resolution path.
|
||||
*
|
||||
* **Forced-mode parity (`REGISTRY_PRIMARY_KOTLIN=1`):** 208/208
|
||||
* fixtures pass after the migration sub-issues #1758–#1763, the
|
||||
* **Coverage:** 208/208 fixtures pass after the migration sub-issues
|
||||
* #1758–#1763, the
|
||||
* companion/instance dispatch fix #1756, and the lambda scopes
|
||||
* fix #1757. Covers core import, receiver, companion, default-param,
|
||||
* vararg, constructor, local assignment-chain, collection-iteration,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { typeConfig as phpConfig } from '../type-extractors/php.js';
|
|||
import { phpExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { phpImportConfig } from '../import-resolvers/configs/php.js';
|
||||
import { extractPhpNamedBindings } from '../named-bindings/php.js';
|
||||
import { PHP_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { findDescendant, extractStringContent, type SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
|
|
@ -37,7 +36,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { phpVariableConfig } from '../variable-extractors/configs/php.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { phpCallConfig } from '../call-extractors/configs/php.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'echo',
|
||||
|
|
@ -289,13 +287,11 @@ export const phpProvider = defineLanguage({
|
|||
typeConfig: phpConfig,
|
||||
exportChecker: phpExportChecker,
|
||||
importResolver: createImportResolver(phpImportConfig),
|
||||
namedBindingExtractor: extractPhpNamedBindings,
|
||||
callExtractor: createCallExtractor(phpCallConfig),
|
||||
fieldExtractor: createFieldExtractor(phpFieldConfig),
|
||||
methodExtractor: createMethodExtractor(phpMethodConfig),
|
||||
variableExtractor: createVariableExtractor(phpVariableConfig),
|
||||
classExtractor: createClassExtractor(phpClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.PHP),
|
||||
descriptionExtractor: phpDescriptionExtractor,
|
||||
isRouteFile: isPhpRouteFile,
|
||||
builtInNames: BUILT_INS,
|
||||
|
|
|
|||
|
|
@ -308,11 +308,11 @@ export function emitPhpScopeCaptures(
|
|||
* the registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
|
||||
* (mirrors C# `synthesizeCsharpInheritanceReferences` / C++
|
||||
* `emitCppInheritanceCaptures`). Without this, PHP inheritance edges came only
|
||||
* from the legacy `@heritage.*` path, which the worker pipeline drops for
|
||||
* registry-primary languages (issue #1951).
|
||||
* from the legacy heritage-capture leg (removed in #942), which the worker
|
||||
* pipeline drops for registry-primary languages (issue #1951).
|
||||
*
|
||||
* Scope matches the legacy PHP heritage query (tree-sitter-queries.ts
|
||||
* PHP_QUERIES @heritage.extends / @heritage.implements / @heritage.trait):
|
||||
* PHP_QUERIES extends / implements / trait-use captures):
|
||||
*
|
||||
* 1. `class_declaration` > `base_clause` > [(name) (qualified_name)] — extends
|
||||
* 2. `class_declaration` > `class_interface_clause` > [(name) (qualified_name)] — implements
|
||||
|
|
@ -325,9 +325,8 @@ export function emitPhpScopeCaptures(
|
|||
* lookup name is normalized to its bare simple identifier (`Foo\Bar\Base` →
|
||||
* `Base`) to match the V1 simple-name `findClassBindingInScope` contract.
|
||||
*
|
||||
* NOTE (#1951 trait-use parity): legacy emits trait-use as an IMPLEMENTS edge
|
||||
* (`heritage.trait` → `trait-impl` → IMPLEMENTS in heritage-processor.ts), and
|
||||
* the central pass matches it — `preEmitInheritanceEdges` (run.ts) maps a
|
||||
* NOTE (#1951 trait-use parity): a PHP `use Trait;` is emitted as an IMPLEMENTS
|
||||
* edge — `preEmitInheritanceEdges` (run.ts) maps a
|
||||
* resolved `Interface` OR `Trait` target to IMPLEMENTS (`type === 'Interface'
|
||||
* || type === 'Trait' ? 'IMPLEMENTS' : 'EXTENDS'`), so `use Trait` resolves to
|
||||
* IMPLEMENTS on both the legacy and registry-primary paths.
|
||||
|
|
|
|||
|
|
@ -56,10 +56,22 @@ export function splitNamespaceUseDeclaration(stmtNode: SyntaxNode): CaptureMatch
|
|||
return decomposeGrouped(stmtNode, groupNode, qualifier);
|
||||
}
|
||||
|
||||
// Single use clause (possibly aliased).
|
||||
const spec = parseSingleUseClause(stmtNode, qualifier);
|
||||
if (spec === null) return [];
|
||||
return [buildImportMatch(stmtNode, spec)];
|
||||
// Non-grouped: iterate namespace_use_clause children (one per comma-separated
|
||||
// import in `use A, B, C;`). The first import is also a namespace_use_clause
|
||||
// in the grammar version used by the monorepo; namespace_name child handling
|
||||
// was removed after confirming the clause wrapper covers all positions.
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (let i = 0; i < stmtNode.namedChildCount; i++) {
|
||||
const child = stmtNode.namedChild(i);
|
||||
if (child === null || child.type !== 'namespace_use_clause') continue;
|
||||
const spec = parseUseClause(child, qualifier);
|
||||
if (spec !== null) {
|
||||
out.push(buildImportMatch(stmtNode, spec));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Qualifier detection ────────────────────────────────────────────────────
|
||||
|
|
@ -81,29 +93,6 @@ function detectQualifier(node: SyntaxNode): PhpImportKind {
|
|||
return 'namespace';
|
||||
}
|
||||
|
||||
// ── Single clause parsing ──────────────────────────────────────────────────
|
||||
|
||||
function parseSingleUseClause(node: SyntaxNode, qualifier: PhpImportKind): PhpImportSpec | null {
|
||||
// A plain `namespace_use_declaration` has one or more
|
||||
// `namespace_use_clause` named children (each clause is one import,
|
||||
// comma-separated for multiple). For the single case there is one.
|
||||
const clause = findNamedChild(node, 'namespace_use_clause');
|
||||
if (clause !== null) return parseUseClause(clause, qualifier);
|
||||
|
||||
// Older grammar versions may put the qualified_name directly under
|
||||
// the declaration node. Check for a qualified_name or name child.
|
||||
const qualName = findNamedChild(node, 'qualified_name') ?? findNamedChild(node, 'name');
|
||||
if (qualName === null) return null;
|
||||
const source = qualName.text.trim();
|
||||
if (source === '') return null;
|
||||
return {
|
||||
kind: qualifier,
|
||||
source,
|
||||
name: lastSegment(source),
|
||||
atNode: node,
|
||||
};
|
||||
}
|
||||
|
||||
function parseUseClause(clause: SyntaxNode, qualifier: PhpImportKind): PhpImportSpec | null {
|
||||
// namespace_use_clause:
|
||||
// qualified_name (or name)
|
||||
|
|
@ -162,8 +151,10 @@ function decomposeGrouped(
|
|||
groupNode: SyntaxNode,
|
||||
outerQualifier: PhpImportKind,
|
||||
): CaptureMatch[] {
|
||||
// The prefix is the qualified_name that precedes the `{...}` group.
|
||||
const prefixNode = findNamedChild(stmtNode, 'qualified_name') ?? findNamedChild(stmtNode, 'name');
|
||||
// The prefix is the namespace_name that precedes the `{...}` group.
|
||||
// tree-sitter-php emits the leading path as a namespace_name child,
|
||||
// not qualified_name.
|
||||
const prefixNode = findNamedChild(stmtNode, 'namespace_name') ?? findNamedChild(stmtNode, 'name');
|
||||
const prefix = prefixNode?.text.trim() ?? '';
|
||||
|
||||
const out: CaptureMatch[] = [];
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const PHP_SCOPE_QUERY = `
|
|||
(interface_declaration) @scope.class
|
||||
(trait_declaration) @scope.class
|
||||
(enum_declaration) @scope.class
|
||||
(anonymous_class) @scope.class
|
||||
|
||||
(method_declaration) @scope.function
|
||||
(function_definition) @scope.function
|
||||
|
|
@ -74,6 +75,10 @@ const PHP_SCOPE_QUERY = `
|
|||
(enum_declaration
|
||||
name: (name) @declaration.name) @declaration.enum
|
||||
|
||||
;; Enum case
|
||||
(enum_case
|
||||
name: (name) @declaration.name) @declaration.const
|
||||
|
||||
;; ── Declarations — methods / functions / constructors ─────────────────────
|
||||
|
||||
(method_declaration
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
* LanguageProvider, following the Strategy pattern used by the pipeline.
|
||||
*
|
||||
* Key Python traits:
|
||||
* - importSemantics: 'namespace' (Python uses namespace imports, not wildcard)
|
||||
* - mroStrategy: 'c3' (Python C3 linearization for multiple inheritance)
|
||||
* - namedBindingExtractor: present (from X import Y)
|
||||
*/
|
||||
|
||||
import type { NodeLabel } from 'gitnexus-shared';
|
||||
|
|
@ -20,7 +18,6 @@ import { typeConfig as pythonConfig } from '../type-extractors/python.js';
|
|||
import { pythonExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { pythonImportConfig } from '../import-resolvers/configs/python.js';
|
||||
import { extractPythonNamedBindings } from '../named-bindings/python.js';
|
||||
import { PYTHON_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { pythonConfig as pythonFieldConfig } from '../field-extractors/configs/python.js';
|
||||
|
|
@ -30,7 +27,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { pythonVariableConfig } from '../variable-extractors/configs/python.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { pythonCallConfig } from '../call-extractors/configs/python.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import type { CaptureMap } from '../language-provider.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import {
|
||||
|
|
@ -126,15 +122,12 @@ export const pythonProvider = defineLanguage({
|
|||
typeConfig: pythonConfig,
|
||||
exportChecker: pythonExportChecker,
|
||||
importResolver: createImportResolver(pythonImportConfig),
|
||||
namedBindingExtractor: extractPythonNamedBindings,
|
||||
importSemantics: 'namespace',
|
||||
mroStrategy: 'c3',
|
||||
callExtractor: createCallExtractor(pythonCallConfig),
|
||||
fieldExtractor: createFieldExtractor(pythonFieldConfig),
|
||||
methodExtractor: createMethodExtractor(pythonMethodConfig),
|
||||
variableExtractor: createVariableExtractor(pythonVariableConfig),
|
||||
classExtractor: createClassExtractor(pythonClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Python),
|
||||
descriptionExtractor: pythonDescriptionExtractor,
|
||||
builtInNames: BUILT_INS,
|
||||
labelOverride: pythonFunctionDefinitionLabel,
|
||||
|
|
|
|||
|
|
@ -178,13 +178,13 @@ export function emitPythonScopeCaptures(
|
|||
* (mirrors C#'s `synthesizeCsharpInheritanceReferences` / C++'s
|
||||
* `emitCppInheritanceCaptures` / TypeScript's `synthesizeTsInheritanceReferences`).
|
||||
* Without this, Python inheritance edges came only from the legacy
|
||||
* `@heritage.*` path, which is dropped for registry-primary languages in the
|
||||
* worker pipeline (issue #1951).
|
||||
* heritage-capture leg (removed in #942), which is dropped for registry-primary
|
||||
* languages in the worker pipeline (issue #1951).
|
||||
*
|
||||
* Scope matches the legacy Python heritage leg (config-driven since #1940):
|
||||
* every direct base in the `superclasses` `argument_list`, resolved to its bare
|
||||
* simple name. Three base shapes that the previous synth DROPPED — and so
|
||||
* silently omitted in production while the legacy `@heritage` leg captured them
|
||||
* silently omitted in production while the legacy heritage leg captured them
|
||||
* — are now handled (#1951):
|
||||
*
|
||||
* - `class C(pkg.Base)` → `attribute` (trailing `.attribute` id → `Base`)
|
||||
|
|
|
|||
|
|
@ -19,29 +19,6 @@ const PYTHON_SCOPE_QUERY = `
|
|||
(class_definition
|
||||
name: (identifier) @declaration.name) @declaration.class
|
||||
|
||||
;; Heritage — bare identifier
|
||||
;; NOTE: captures.ts on main already synthesizes @reference.inherits for
|
||||
;; qualified bases via #1951/#1956. These @heritage.* patterns are redundant
|
||||
;; with that synthesis but kept as documentation and a safety net for the
|
||||
;; generic heritage extractor path. They produce topicOf edges that the
|
||||
;; resolution pipeline ignores when the synthesis path wins.
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclasses: (argument_list
|
||||
(identifier) @heritage.extends)) @heritage
|
||||
|
||||
;; Heritage — qualified base (module.Class)
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclasses: (argument_list
|
||||
(attribute) @heritage.extends)) @heritage
|
||||
|
||||
;; Heritage — subscripted/generic base (Generic[T])
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclasses: (argument_list
|
||||
(subscript) @heritage.extends)) @heritage
|
||||
|
||||
(function_definition
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { rubyVariableConfig } from '../variable-extractors/configs/ruby.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { rubyCallConfig } from '../call-extractors/configs/ruby.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import { rubyHeritageConfig } from '../heritage-extractors/configs/ruby.js';
|
||||
import { maybeRewriteRubyBareCallToSelf } from '../utils/ruby-self-call.js';
|
||||
import { findEnclosingClassInfo } from '../utils/ast-helpers.js';
|
||||
import type { DispatchDecision, ImplicitReceiverOverride } from '../call-types.js';
|
||||
import {
|
||||
emitRubyScopeCaptures,
|
||||
rubyArityCompatibility,
|
||||
|
|
@ -140,7 +135,8 @@ const BUILT_INS: ReadonlySet<string> = new Set([
|
|||
/**
|
||||
* Remaps `class << self` (singleton_class) to its enclosing class/module for
|
||||
* receiver inference. A `singleton_class` node is not itself a type — walking
|
||||
* up to the real owner lets `inferImplicitReceiver` set `hint='singleton'`.
|
||||
* up to the real owner resolves the singleton's enclosing class for the
|
||||
* `resolveEnclosingOwner` scope-resolution hook.
|
||||
* Returns null for orphaned singleton_class (no enclosing class/module found).
|
||||
* All other container types are returned as-is.
|
||||
*/
|
||||
|
|
@ -191,7 +187,6 @@ export const rubyProvider = defineLanguage({
|
|||
exportChecker: rubyExportChecker,
|
||||
importResolver: createImportResolver(rubyImportConfig),
|
||||
callRouter: routeRubyCall,
|
||||
importSemantics: 'wildcard-leaf',
|
||||
callExtractor: createCallExtractor(rubyCallConfig),
|
||||
resolveEnclosingOwner: rubyResolveEnclosingOwner,
|
||||
fieldExtractor: createFieldExtractor(rubyFieldConfig),
|
||||
|
|
@ -201,71 +196,12 @@ export const rubyProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(rubyVariableConfig),
|
||||
classExtractor: createClassExtractor(rubyClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(rubyHeritageConfig),
|
||||
labelOverride: rubyLabelOverride,
|
||||
// Ruby MRO is kind-aware: prepend providers beat the class's own method,
|
||||
// which in turn beats include providers. See `lookupMethodByOwnerWithMRO`
|
||||
// in `model/resolve.ts` for the walk order.
|
||||
// which in turn beats include providers. The graph-level MRO phase
|
||||
// (mro-processor.ts) and per-resolver buildMro consume this strategy.
|
||||
mroStrategy: 'ruby-mixin',
|
||||
|
||||
// ── DAG hooks ────────────────────────────────────────────────────
|
||||
//
|
||||
// DAG stage 3: rewrite bare calls (e.g. `serialize` in Account#call_serialize)
|
||||
// as `self.serialize` so they route through owner-scoped MRO instead of
|
||||
// global free-call lookup. `dispatchKind` goes into `hint` for stage 4.
|
||||
inferImplicitReceiver: ({
|
||||
calledName,
|
||||
callForm,
|
||||
receiverName,
|
||||
receiverTypeName,
|
||||
callNode,
|
||||
filePath,
|
||||
}): ImplicitReceiverOverride | null => {
|
||||
// Only fire when no receiver has been resolved already.
|
||||
if (receiverName || receiverTypeName) return null;
|
||||
const enclosing = findEnclosingClassInfo(callNode, filePath, rubyResolveEnclosingOwner);
|
||||
const rewrite = maybeRewriteRubyBareCallToSelf(
|
||||
calledName,
|
||||
callForm,
|
||||
callNode,
|
||||
enclosing?.className ?? null,
|
||||
{ isBuiltInName: (n) => BUILT_INS.has(n), mroStrategy: 'ruby-mixin' },
|
||||
);
|
||||
if (!rewrite) return null;
|
||||
return {
|
||||
callForm: rewrite.callForm,
|
||||
receiverName: rewrite.receiverName,
|
||||
receiverTypeName: rewrite.receiverTypeName,
|
||||
receiverSource: 'implicit-self',
|
||||
hint: rewrite.dispatchKind, // 'instance' | 'singleton'
|
||||
};
|
||||
},
|
||||
|
||||
// DAG stage 4: two Ruby dispatch overrides —
|
||||
// implicit-self: MRO walk first, fallback to free-arity-narrowed on miss.
|
||||
// class-as-receiver: singleton ancestry (extend providers only); miss null-routes.
|
||||
selectDispatch: ({ receiverSource, hint }): DispatchDecision | null => {
|
||||
if (receiverSource === 'implicit-self') {
|
||||
// hint='instance' → instance ancestry (prepend→direct→include, see mro-strategy.ts § 'ruby-mixin')
|
||||
// hint='singleton' → singleton ancestry (extend providers only; miss null-routes)
|
||||
const ancestryView: 'instance' | 'singleton' =
|
||||
hint === 'singleton' ? 'singleton' : 'instance';
|
||||
return {
|
||||
primary: 'owner-scoped',
|
||||
fallback: 'free-arity-narrowed',
|
||||
ancestryView,
|
||||
};
|
||||
}
|
||||
if (receiverSource === 'class-as-receiver') {
|
||||
// Class constant receiver (e.g. Account.log): singleton ancestry only; miss null-routes.
|
||||
return {
|
||||
primary: 'owner-scoped',
|
||||
ancestryView: 'singleton',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
builtInNames: BUILT_INS,
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
|
||||
emitScopeCaptures: emitRubyScopeCaptures,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { synthesizeRubyReceiverBinding, findEnclosingClassOrModule } from './rec
|
|||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
import { splitQualifiedName } from '../../utils/qualified-name.js';
|
||||
import { encodeMarker } from '../../utils/heritage-marker.js';
|
||||
|
||||
const FUNCTION_NODE_TYPES = ['method', 'singleton_method'] as const;
|
||||
const HERITAGE_CALL_NAMES: ReadonlySet<string> = new Set(['include', 'extend', 'prepend']);
|
||||
|
|
@ -193,7 +194,7 @@ export function emitRubyScopeCaptures(
|
|||
'@import.source': syntheticCapture(
|
||||
'@import.source',
|
||||
callNode,
|
||||
`__heritage__:${callName}:${mixinName}:${ownerName}`,
|
||||
encodeMarker('heritage', [callName, mixinName, ownerName]),
|
||||
),
|
||||
'@import.name': syntheticCapture('@import.name', callNode, mixinName),
|
||||
});
|
||||
|
|
@ -227,7 +228,7 @@ export function emitRubyScopeCaptures(
|
|||
'@import.source': syntheticCapture(
|
||||
'@import.source',
|
||||
callNode,
|
||||
`__property__:${callName}:${propName}:${ownerName}`,
|
||||
encodeMarker('property', [callName, propName, ownerName]),
|
||||
),
|
||||
'@import.name': syntheticCapture('@import.name', callNode, propName),
|
||||
});
|
||||
|
|
@ -470,11 +471,11 @@ export function emitRubyScopeCaptures(
|
|||
// Emit `@reference.inherits` captures so the registry-primary scope-
|
||||
// resolution path produces EXTENDS edges (issue #1951). This mirrors the
|
||||
// C#/C++ inheritance synthesis: Ruby's superclass edges previously came
|
||||
// only from the legacy `@heritage.extends` query, which the worker
|
||||
// pipeline drops for registry-primary languages → 0 inheritance edges in
|
||||
// worker mode. Mixins (include/extend/prepend) are NOT touched here — they
|
||||
// only from the legacy heritage-capture query (removed in #942), which the
|
||||
// worker pipeline drops for registry-primary languages → 0 inheritance edges
|
||||
// in worker mode. Mixins (include/extend/prepend) are NOT touched here — they
|
||||
// flow through `emitHeritageEdges` (the `__heritage__:` import path above),
|
||||
// an independent lane that stays intact when legacy @heritage is gated off.
|
||||
// an independent lane that stays intact when the legacy heritage leg is gated off.
|
||||
out.push(...synthesizeRubySuperclassReferences(tree.rootNode));
|
||||
|
||||
return out;
|
||||
|
|
@ -488,18 +489,16 @@ export function emitRubyScopeCaptures(
|
|||
* Scope is `class` nodes whose `superclass` field holds either a bare
|
||||
* `constant` base (`class D < Super`) or a qualified/scoped
|
||||
* `scope_resolution` base (`class C < Outer::Super`, `class E < A::B::C`) —
|
||||
* exactly the two shapes the config-driven legacy `@heritage.extends`
|
||||
* alternation now captures (heritage-extractors/configs/ruby.ts
|
||||
* `rubyHeritageShapes: ['constant', 'scope_resolution']`):
|
||||
*
|
||||
* (class
|
||||
* name: (constant) @heritage.class
|
||||
* superclass: (superclass
|
||||
* [(constant) (scope_resolution)] @heritage.extends)) @heritage
|
||||
* exactly the two shapes the config-driven legacy heritage alternation
|
||||
* captured (heritage-extractors/configs/ruby.ts
|
||||
* `rubyHeritageShapes: ['constant', 'scope_resolution']`). In prose: the
|
||||
* legacy query matched a `class` whose name is a `constant` and whose
|
||||
* `superclass` is either a `constant` or a `scope_resolution`, capturing the
|
||||
* superclass constant as the inherited base.
|
||||
*
|
||||
* Previously this pass emitted only for a direct `(constant)` child, so the
|
||||
* production registry-primary path silently dropped `Outer::Super`
|
||||
* superclasses while the legacy @heritage leg captured them — the exact
|
||||
* superclasses while the legacy heritage leg captured them — the exact
|
||||
* EXTENDS/IMPLEMENTS-drop bug of #1951.
|
||||
*
|
||||
* THE PARITY CONTRACT: the `@reference.name` bare text must equal the legacy
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { resolveRubyImportInternal } from '../../import-resolvers/ruby.js';
|
||||
import { buildSuffixIndex } from '../../import-resolvers/utils.js';
|
||||
import { isHeritageMarker } from '../../utils/heritage-marker.js';
|
||||
|
||||
export interface RubyResolveContext {
|
||||
readonly fromFile: string;
|
||||
|
|
@ -37,7 +38,7 @@ export function resolveRubyImportTarget(
|
|||
_resolutionConfig?: unknown,
|
||||
): string | readonly string[] | null {
|
||||
if (!targetRaw) return null;
|
||||
if (targetRaw.startsWith('__heritage__:') || targetRaw.startsWith('__property__:')) return null;
|
||||
if (isHeritageMarker(targetRaw)) return null;
|
||||
|
||||
const fromNormalized = fromFile.replace(/\\/g, '/');
|
||||
const fromDir = fromNormalized.includes('/')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
|
||||
import { isHeritageMarker } from '../../utils/heritage-marker.js';
|
||||
|
||||
// ─── interpretImport ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ export function interpretRubyImport(captures: CaptureMatch): ParsedImport | null
|
|||
|
||||
// Heritage-encoded imports (__heritage__:include:Serializable:User)
|
||||
// are stored as namespace imports so emitHeritageEdges can read them.
|
||||
if (source.startsWith('__heritage__:') || source.startsWith('__property__:')) {
|
||||
if (isHeritageMarker(source)) {
|
||||
const name = captures['@import.name']?.text ?? source;
|
||||
return { kind: 'namespace', localName: name, importedName: name, targetRaw: source };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,29 @@ import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
|
|||
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
||||
import type { KnowledgeGraph } from '../../../graph/types.js';
|
||||
import { generateId } from '../../../../lib/utils.js';
|
||||
import { decodeMarker } from '../../utils/heritage-marker.js';
|
||||
|
||||
const HERITAGE_PREFIX = '__heritage__:';
|
||||
const PROPERTY_PREFIX = '__property__:';
|
||||
/**
|
||||
* #1991: resolve a BARE mixin reference (`include Loggable`) to a nested module by
|
||||
* the INCLUDING class's lexical scope — Ruby looks up a constant in the innermost
|
||||
* enclosing scope first. For owner `App.S`, try `App.Loggable`, then walk outward.
|
||||
* Returns undefined if no enclosing-scope-qualified module exists.
|
||||
*/
|
||||
function qualifyMixinByOwnerScope(
|
||||
mixinName: string,
|
||||
ownerName: string,
|
||||
graphIdByName: ReadonlyMap<string, string>,
|
||||
): string | undefined {
|
||||
let prefix = ownerName;
|
||||
let dot = prefix.lastIndexOf('.');
|
||||
while (dot !== -1) {
|
||||
prefix = prefix.slice(0, dot);
|
||||
const g = graphIdByName.get(`${prefix}.${mixinName}`);
|
||||
if (g !== undefined) return g;
|
||||
dot = prefix.lastIndexOf('.');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function emitRubyMixinEdges(
|
||||
graph: KnowledgeGraph,
|
||||
|
|
@ -19,15 +39,14 @@ function emitRubyMixinEdges(
|
|||
nodeLookup: GraphNodeLookup,
|
||||
): void {
|
||||
const graphIdByName = new Map<string, string>();
|
||||
// Secondary tail -> graphId map (first-wins). The `__heritage__` marker carries
|
||||
// the mixin TARGET as the bare written name (`arg.text`, e.g. `Loggable`), not
|
||||
// its full qualifiedName, so a nested mixin module included by its short name
|
||||
// (`include Loggable` where it is `App::Loggable`) misses the full-qn map and
|
||||
// its IMPLEMENTS edge is silently dropped (#1982 follow-up). The tail fallback
|
||||
// recovers it. OWNER (`className`) lookups stay full-qn only, preserving
|
||||
// same-tail owner disambiguation; only the under-qualified mixin reference
|
||||
// falls back, and a genuine same-tail mixin tie there resolves first-wins.
|
||||
const graphIdByTail = new Map<string, string>();
|
||||
// Secondary tail -> graphId map. The `__heritage__` marker carries the mixin
|
||||
// TARGET as the bare written name (`arg.text`, e.g. `Loggable`), not its full
|
||||
// qualifiedName, so a nested mixin module included by its short name misses the
|
||||
// full-qn map. We first resolve it lexically by the including class's enclosing
|
||||
// scope (`qualifyMixinByOwnerScope`); this tail map is the last resort. A genuine
|
||||
// same-tail collision is mapped to `null` so we REFUSE to guess (#1991) rather
|
||||
// than the old first-wins, which cross-wired App::Loggable / Web::Loggable.
|
||||
const graphIdByTail = new Map<string, string | null>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const def of parsed.localDefs) {
|
||||
if (!isClassLike(def.type)) continue;
|
||||
|
|
@ -43,7 +62,12 @@ function emitRubyMixinEdges(
|
|||
graphIdByName.set(fullName, graphId);
|
||||
const dot = fullName.lastIndexOf('.');
|
||||
const tail = dot === -1 ? fullName : fullName.slice(dot + 1);
|
||||
if (tail.length > 0 && !graphIdByTail.has(tail)) graphIdByTail.set(tail, graphId);
|
||||
if (tail.length > 0) {
|
||||
const existingTail = graphIdByTail.get(tail);
|
||||
if (existingTail === undefined) graphIdByTail.set(tail, graphId);
|
||||
else if (existingTail !== null && existingTail !== graphId)
|
||||
graphIdByTail.set(tail, null); // same-tail collision — refuse to guess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,14 +83,21 @@ function emitRubyMixinEdges(
|
|||
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const imp of parsed.parsedImports) {
|
||||
if (!imp.targetRaw.startsWith(HERITAGE_PREFIX)) continue;
|
||||
const parts = imp.targetRaw.slice(HERITAGE_PREFIX.length).split(':');
|
||||
const decoded = decodeMarker(imp.targetRaw);
|
||||
if (decoded?.kind !== 'heritage') continue;
|
||||
const parts = decoded.fields;
|
||||
if (parts.length < 3) continue;
|
||||
const [kind, mixinName, className] = parts;
|
||||
const classGraphId = graphIdByName.get(className!);
|
||||
// Owner stays full-qn; the mixin target may be written by short name and
|
||||
// miss the full-qn map, so fall back to the simple-tail map (#1982).
|
||||
const mixinGraphId = graphIdByName.get(mixinName!) ?? graphIdByTail.get(mixinName!);
|
||||
// Owner stays full-qn. The mixin target may be written by short name and miss
|
||||
// the full-qn map; resolve it lexically by the including class's enclosing
|
||||
// scope (`App::S` + `Loggable` -> `App::Loggable`), then fall back to the tail
|
||||
// map ONLY when unambiguous — never first-wins on a collision (#1982/#1991).
|
||||
const mixinGraphId =
|
||||
graphIdByName.get(mixinName!) ??
|
||||
qualifyMixinByOwnerScope(mixinName!, className!, graphIdByName) ??
|
||||
graphIdByTail.get(mixinName!) ??
|
||||
undefined;
|
||||
if (classGraphId === undefined || mixinGraphId === undefined) continue;
|
||||
const edgeKey = `${classGraphId}->${mixinGraphId}:${kind}`;
|
||||
if (emitted.has(edgeKey)) continue;
|
||||
|
|
@ -95,8 +126,9 @@ function emitRubyMixinEdges(
|
|||
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const imp of parsed.parsedImports) {
|
||||
if (!imp.targetRaw.startsWith(PROPERTY_PREFIX)) continue;
|
||||
const parts = imp.targetRaw.slice(PROPERTY_PREFIX.length).split(':');
|
||||
const decoded = decodeMarker(imp.targetRaw);
|
||||
if (decoded?.kind !== 'property') continue;
|
||||
const parts = decoded.fields;
|
||||
if (parts.length < 3) continue;
|
||||
const [_attrKind, propName, className] = parts;
|
||||
const classGraphId = graphIdByName.get(className!);
|
||||
|
|
@ -171,8 +203,8 @@ function buildRubyMro(
|
|||
|
||||
// Step 4: Reorder MRO per Ruby semantics.
|
||||
// Order: prepend (reversed) → direct extends chain → include (reversed).
|
||||
// `extend` is excluded — it belongs to singleton dispatch only (legacy
|
||||
// `getInstanceAncestry` in heritage-map.ts explicitly drops extend entries).
|
||||
// `extend` is excluded — it belongs to singleton dispatch only (the
|
||||
// instance-ancestry walk drops extend entries).
|
||||
// Reversed because Ruby declaration order means last-declared wins
|
||||
// (prepend B; prepend A → B checked before A).
|
||||
for (const defId of defIdByGraphId.values()) {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
* LanguageProvider, following the Strategy pattern used by the pipeline.
|
||||
*
|
||||
* Key Rust traits:
|
||||
* - importSemantics: 'named' (Rust has use X::{a, b})
|
||||
* - mroStrategy: 'qualified-syntax' (Rust uses trait qualification, not MRO)
|
||||
* - namedBindingExtractor: present (use X::{a, b} extracts named bindings)
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
@ -20,7 +18,6 @@ import { typeConfig as rustConfig } from '../type-extractors/rust.js';
|
|||
import { rustExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { rustImportConfig } from '../import-resolvers/configs/rust.js';
|
||||
import { extractRustNamedBindings } from '../named-bindings/rust.js';
|
||||
import { RUST_QUERIES } from '../tree-sitter-queries.js';
|
||||
import type { AstFrameworkPatternConfig } from '../language-provider.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
|
|
@ -31,7 +28,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { rustVariableConfig } from '../variable-extractors/configs/rust.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { rustCallConfig } from '../call-extractors/configs/rust.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
emitRustScopeCaptures,
|
||||
rustArityCompatibility,
|
||||
|
|
@ -170,7 +166,6 @@ export const rustProvider = defineLanguage({
|
|||
typeConfig: rustConfig,
|
||||
exportChecker: rustExportChecker,
|
||||
importResolver: createImportResolver(rustImportConfig),
|
||||
namedBindingExtractor: extractRustNamedBindings,
|
||||
mroStrategy: 'qualified-syntax',
|
||||
callExtractor: createCallExtractor(rustCallConfig),
|
||||
fieldExtractor: createFieldExtractor(rustFieldConfig),
|
||||
|
|
@ -180,7 +175,6 @@ export const rustProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(rustVariableConfig),
|
||||
classExtractor: createClassExtractor(rustClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Rust),
|
||||
builtInNames: BUILT_INS,
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
|
||||
emitScopeCaptures: emitRustScopeCaptures,
|
||||
|
|
|
|||
|
|
@ -170,8 +170,8 @@ export function emitRustScopeCaptures(
|
|||
/**
|
||||
* Synthesize `@reference.inherits` captures from Rust trait `impl` blocks so
|
||||
* the registry-primary scope-resolution path can emit the IMPLEMENTS edge for
|
||||
* `impl Trait for Struct` (mirrors the legacy `@heritage.trait`/`@heritage.class`
|
||||
* path, which the worker pipeline drops for registry-primary languages — #1951).
|
||||
* `impl Trait for Struct` (mirrors the legacy heritage-capture leg, removed in
|
||||
* #942, which the worker pipeline drops for registry-primary languages — #1951).
|
||||
*
|
||||
* Rust inheritance is structurally unlike a base list on a type declaration:
|
||||
* the relationship lives on `impl_item { trait: T, type: S }`, meaning
|
||||
|
|
@ -217,8 +217,8 @@ function synthesizeRustInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
|
|||
|
||||
/**
|
||||
* Normalize a `trait:` / `type:` impl_item field to the base's trailing bare
|
||||
* `type_identifier`, matching exactly the node shapes the legacy `@heritage`
|
||||
* query accepts (kept at parity — see the `impl_item` heritage arm in
|
||||
* `type_identifier`, matching exactly the node shapes the legacy heritage
|
||||
* query accepted (kept at parity — see the `impl_item` heritage arm in
|
||||
* tree-sitter-queries.ts):
|
||||
* - `type_identifier` → the node itself
|
||||
* - `scoped_type_identifier name: (type_identifier)` → the trailing `name:` id
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { generateId } from '../../../../lib/utils.js';
|
|||
* sites synthesized in `captures.ts` carry the trait `T` as `site.name` (target)
|
||||
* and the struct `S` as `site.explicitReceiver.name` (source); this hook reads
|
||||
* them back and emits the IMPLEMENTS edge with source `S`, target `T`, and the
|
||||
* legacy `'trait-impl'` reason — matching the legacy `@heritage` DAG (#1951).
|
||||
* legacy `'trait-impl'` reason — matching the legacy heritage DAG (#1951).
|
||||
*
|
||||
* Resolution is scope-aware and import-aware, mirroring the shared
|
||||
* `preEmitInheritanceEdges` pass: both `S` and `T` resolve from the `impl`
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@
|
|||
*
|
||||
* Assembles all Swift-specific ingestion capabilities into a single
|
||||
* LanguageProvider, following the Strategy pattern used by the pipeline.
|
||||
*
|
||||
* Key Swift traits:
|
||||
* - importSemantics: 'wildcard-leaf' (Swift imports entire modules)
|
||||
* - heritageDefaultEdge: 'IMPLEMENTS' (protocols are more common than class inheritance)
|
||||
* - implicitImportWirer: all files in the same SPM target see each other
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
@ -21,7 +16,6 @@ import { swiftExportChecker } from '../export-detection.js';
|
|||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { swiftImportConfig } from '../import-resolvers/configs/swift.js';
|
||||
import { SWIFT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import type { SwiftPackageConfig } from '../language-config.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { swiftConfig as swiftFieldConfig } from '../field-extractors/configs/swift.js';
|
||||
|
|
@ -31,7 +25,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { swiftVariableConfig } from '../variable-extractors/configs/swift.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { swiftCallConfig } from '../call-extractors/configs/swift.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
emitSwiftScopeCaptures,
|
||||
interpretSwiftImport,
|
||||
|
|
@ -43,93 +36,6 @@ import {
|
|||
swiftArityCompatibility,
|
||||
} from './swift/index.js';
|
||||
|
||||
/**
|
||||
* Group Swift files by SPM target for implicit module visibility.
|
||||
* If SwiftPackageConfig is available, use target -> directory mappings.
|
||||
* Otherwise, group all Swift files under a single "default" target
|
||||
* (assumes a single-module Xcode project).
|
||||
*/
|
||||
function groupSwiftFilesByTarget(
|
||||
swiftFiles: string[],
|
||||
swiftPackageConfig: SwiftPackageConfig | null,
|
||||
): Map<string, string[]> {
|
||||
// No SPM config -> single target (common for Xcode projects)
|
||||
if (!swiftPackageConfig || swiftPackageConfig.targets.size === 0) {
|
||||
return new Map([['__default__', swiftFiles]]);
|
||||
}
|
||||
|
||||
// Pre-convert target dirs to normalized prefix format once
|
||||
const targets = [...swiftPackageConfig.targets.entries()].map(([name, dir]) => ({
|
||||
name,
|
||||
prefix: dir.replace(/\\/g, '/') + '/',
|
||||
}));
|
||||
|
||||
const groups = new Map<string, string[]>();
|
||||
const defaultGroup: string[] = [];
|
||||
|
||||
for (const file of swiftFiles) {
|
||||
const normalized = file.includes('\\') ? file.replace(/\\/g, '/') : file;
|
||||
let assigned = false;
|
||||
for (const { name, prefix } of targets) {
|
||||
const idx = normalized.indexOf(prefix);
|
||||
if (idx === 0 || (idx > 0 && normalized[idx - 1] === '/')) {
|
||||
let group = groups.get(name);
|
||||
if (!group) {
|
||||
group = [];
|
||||
groups.set(name, group);
|
||||
}
|
||||
group.push(file);
|
||||
assigned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!assigned) defaultGroup.push(file);
|
||||
}
|
||||
|
||||
if (defaultGroup.length > 0) groups.set('__default__', defaultGroup);
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire implicit inter-file imports for Swift.
|
||||
* All files in the same SPM target see each other (full module visibility).
|
||||
* Two fast paths avoid unnecessary work:
|
||||
* 1. No existing imports for src -> emit all (m-1) edges without Set.has checks
|
||||
* 2. Existing imports present -> skip already-connected pairs
|
||||
*/
|
||||
function wireSwiftImplicitImports(
|
||||
swiftFiles: string[],
|
||||
importMap: ReadonlyMap<string, ReadonlySet<string>>,
|
||||
addImportEdge: (src: string, target: string) => void,
|
||||
projectConfig: unknown,
|
||||
): void {
|
||||
const configs = projectConfig as { swiftPackageConfig?: SwiftPackageConfig | null } | null;
|
||||
const targetGroups = groupSwiftFilesByTarget(swiftFiles, configs?.swiftPackageConfig ?? null);
|
||||
|
||||
for (const group of targetGroups.values()) {
|
||||
const m = group.length;
|
||||
if (m <= 1) continue;
|
||||
// All-pairs implicit edges: O(m²) is inherent for full module visibility.
|
||||
for (let i = 0; i < m; i++) {
|
||||
const src = group[i];
|
||||
const existing = importMap.get(src);
|
||||
if (!existing || existing.size === 0) {
|
||||
// Fast path: no prior imports — emit all peers unconditionally
|
||||
for (let j = 0; j < m; j++) {
|
||||
if (i !== j) addImportEdge(src, group[j]);
|
||||
}
|
||||
} else {
|
||||
// Dedup path: skip already-connected pairs
|
||||
for (let j = 0; j < m; j++) {
|
||||
if (i !== j && !existing.has(group[j])) {
|
||||
addImportEdge(src, group[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Swift init/deinit declarations have special names and Constructor label. */
|
||||
const swiftExtractFunctionName = (
|
||||
node: SyntaxNode,
|
||||
|
|
@ -328,8 +234,6 @@ export const swiftProvider = defineLanguage({
|
|||
typeConfig: swiftConfig,
|
||||
exportChecker: swiftExportChecker,
|
||||
importResolver: createImportResolver(swiftImportConfig),
|
||||
importSemantics: 'wildcard-leaf',
|
||||
heritageDefaultEdge: 'IMPLEMENTS',
|
||||
callExtractor: createCallExtractor(swiftCallConfig),
|
||||
fieldExtractor: createFieldExtractor(swiftFieldConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
|
|
@ -338,8 +242,6 @@ export const swiftProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(swiftVariableConfig),
|
||||
classExtractor: createClassExtractor(swiftClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Swift),
|
||||
implicitImportWirer: wireSwiftImplicitImports,
|
||||
orderSameNameTypeCandidates: orderSwiftSameNameTypeCandidates,
|
||||
builtInNames: BUILT_INS,
|
||||
// ── Scope-based resolution hooks (RFC #909 Ring 3, issue #937). See
|
||||
|
|
|
|||
|
|
@ -289,8 +289,8 @@ export function emitSwiftScopeCaptures(
|
|||
// inheritance specifiers and synthesize `@reference.inherits` captures so
|
||||
// the registry-primary path emits EXTENDS / IMPLEMENTS (mirrors C++ /
|
||||
// C# / Java). Without this, Swift inheritance edges came only from the
|
||||
// legacy `@heritage.*` path, which the worker pipeline drops for
|
||||
// registry-primary languages (issue #1951).
|
||||
// legacy heritage-capture leg (removed in #942), which the worker pipeline
|
||||
// drops for registry-primary languages (issue #1951).
|
||||
out.push(...synthesizeSwiftInheritanceReferences(tree.rootNode));
|
||||
|
||||
return out;
|
||||
|
|
@ -301,10 +301,10 @@ export function emitSwiftScopeCaptures(
|
|||
* specifiers so the registry-primary scope-resolution path emits
|
||||
* EXTENDS / IMPLEMENTS edges (mirrors `synthesizeCsharpInheritanceReferences`
|
||||
* / `emitCppInheritanceCaptures`). Without this, Swift inheritance edges came
|
||||
* only from the legacy `@heritage.*` path, dropped for registry-primary
|
||||
* languages in the worker pipeline (issue #1951).
|
||||
* only from the legacy heritage-capture leg (removed in #942), dropped for
|
||||
* registry-primary languages in the worker pipeline (issue #1951).
|
||||
*
|
||||
* Scope matches the legacy SWIFT_QUERIES `@heritage` blocks exactly: a
|
||||
* Scope matches the legacy SWIFT_QUERIES heritage blocks exactly: a
|
||||
* `class_declaration` (class / struct / enum / actor / extension all share
|
||||
* this node) or a `protocol_declaration`, each with an
|
||||
* `(inheritance_specifier inherits_from: (user_type (type_identifier)))`.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
/**
|
||||
* TypeScript and JavaScript language providers.
|
||||
*
|
||||
* Both languages share the same type extraction config (typescriptConfig),
|
||||
* export checker (tsExportChecker), and named binding extractor
|
||||
* (extractTsNamedBindings). They differ in file extensions, tree-sitter
|
||||
* Both languages share the same type extraction config (typescriptConfig)
|
||||
* and export checker (tsExportChecker). They differ in file extensions, tree-sitter
|
||||
* queries (TypeScript grammar has interface/type nodes), and language ID.
|
||||
*/
|
||||
|
||||
|
|
@ -24,7 +23,6 @@ import {
|
|||
typescriptImportConfig,
|
||||
javascriptImportConfig,
|
||||
} from '../import-resolvers/configs/typescript-javascript.js';
|
||||
import { extractTsNamedBindings } from '../named-bindings/typescript.js';
|
||||
import { TYPESCRIPT_QUERIES, JAVASCRIPT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { typescriptFieldExtractor } from '../field-extractors/typescript.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
|
|
@ -44,7 +42,6 @@ import {
|
|||
typescriptCallConfig,
|
||||
javascriptCallConfig,
|
||||
} from '../call-extractors/configs/typescript-javascript.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
ARRAY_METHOD_HOC_BLOCKLIST_SET,
|
||||
DEFAULT_EXPORT_IDENTIFIER_BLOCKLIST_SET,
|
||||
|
|
@ -338,7 +335,6 @@ export const typescriptProvider = defineLanguage({
|
|||
typeConfig: typescriptConfig,
|
||||
exportChecker: tsExportChecker,
|
||||
importResolver: createImportResolver(typescriptImportConfig),
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
callExtractor: createCallExtractor(typescriptCallConfig),
|
||||
fieldExtractor: typescriptFieldExtractor,
|
||||
methodExtractor: createMethodExtractor({
|
||||
|
|
@ -347,7 +343,6 @@ export const typescriptProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(typescriptVariableConfig),
|
||||
classExtractor: createClassExtractor(typescriptClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.TypeScript),
|
||||
builtInNames: BUILT_INS,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
|
|
@ -400,7 +395,6 @@ export const javascriptProvider = defineLanguage({
|
|||
typeConfig: typescriptConfig,
|
||||
exportChecker: tsExportChecker,
|
||||
importResolver: createImportResolver(javascriptImportConfig),
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
callExtractor: createCallExtractor(javascriptCallConfig),
|
||||
fieldExtractor: createFieldExtractor(javascriptConfig),
|
||||
methodExtractor: createMethodExtractor({
|
||||
|
|
@ -409,7 +403,6 @@ export const javascriptProvider = defineLanguage({
|
|||
}),
|
||||
variableExtractor: createVariableExtractor(javascriptVariableConfig),
|
||||
classExtractor: createClassExtractor(javascriptClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.JavaScript),
|
||||
builtInNames: BUILT_INS,
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
|
|
|
|||
|
|
@ -429,13 +429,13 @@ export function emitTsScopeCaptures(
|
|||
* the registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
|
||||
* (mirrors C# `synthesizeCsharpInheritanceReferences` / JS
|
||||
* `synthesizeJsInheritanceReferences`). Without this, TS inheritance edges came
|
||||
* only from the legacy `@heritage.*` path, which the worker pipeline drops for
|
||||
* registry-primary languages — yielding 0 inheritance edges in worker mode
|
||||
* (issue #1951).
|
||||
* only from the legacy heritage-capture leg (removed in #942), which the worker
|
||||
* pipeline drops for registry-primary languages — yielding 0 inheritance edges
|
||||
* in worker mode (issue #1951).
|
||||
*
|
||||
* Scope is intentionally limited to a `class_declaration`'s `class_heritage`
|
||||
* `extends_clause` value + `implements_clause` types, matching the legacy
|
||||
* TypeScript `@heritage` query's class scope (TYPESCRIPT_QUERIES). Generic
|
||||
* TypeScript heritage query's class scope (TYPESCRIPT_QUERIES). Generic
|
||||
* bases agree across both paths: `extends Base<T>` is captured by the legacy
|
||||
* `extends_clause value: (identifier)` already (the `type_arguments` are a
|
||||
* sibling field), and `implements IFoo<T>` is captured by a legacy clause
|
||||
|
|
@ -443,14 +443,12 @@ export function emitTsScopeCaptures(
|
|||
* path keeps parity on SIMPLE (unqualified) generic bases too (#1951).
|
||||
* Qualified bases (`ns.Base`, `ns.Base<T>`, `ns.IFoo<T>`) are ALSO now at parity
|
||||
* (#1956 tri-review U2): the synth resolves them by their member_expression /
|
||||
* nested_type_identifier tail, and the legacy `@heritage` query was widened with
|
||||
* nested_type_identifier tail, and the legacy heritage query was widened with
|
||||
* matching arms (member_expression for extends, nested_type_identifier plain +
|
||||
* generic-wrapped for implements).
|
||||
*
|
||||
* `interface_declaration` / `abstract_class_declaration` heritage is NOT emitted
|
||||
* — the legacy query captures neither, so the registry path keeps parity with
|
||||
* the legacy DAG under the CI scope-parity gate (REGISTRY_PRIMARY_TYPESCRIPT=0
|
||||
* vs =1). The EXTENDS-vs-IMPLEMENTS split is decided downstream from the
|
||||
* by the synth. The EXTENDS-vs-IMPLEMENTS split is decided downstream from the
|
||||
* resolved target's symbol kind in `preEmitInheritanceEdges` (class-extends →
|
||||
* EXTENDS, implements-interface / interface-target → IMPLEMENTS), so all bases
|
||||
* are emitted with the same `inherits` kind here. The base lookup name is
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { typeConfig as typescriptConfig } from '../type-extractors/typescript.js
|
|||
import { tsExportChecker } from '../export-detection.js';
|
||||
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
||||
import { vueImportConfig } from '../import-resolvers/configs/typescript-javascript.js';
|
||||
import { extractTsNamedBindings } from '../named-bindings/typescript.js';
|
||||
import { TYPESCRIPT_QUERIES } from '../tree-sitter-queries.js';
|
||||
import { typescriptFieldExtractor } from '../field-extractors/typescript.js';
|
||||
import { BUILT_INS as TS_BUILT_INS } from './typescript.js';
|
||||
|
|
@ -27,7 +26,6 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { typescriptVariableConfig } from '../variable-extractors/configs/typescript-javascript.js';
|
||||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { typescriptCallConfig } from '../call-extractors/configs/typescript-javascript.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
interpretTsImport,
|
||||
interpretTsTypeBinding,
|
||||
|
|
@ -85,12 +83,10 @@ export const vueProvider = defineLanguage({
|
|||
typeConfig: typescriptConfig,
|
||||
exportChecker: tsExportChecker,
|
||||
importResolver: createImportResolver(vueImportConfig),
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
callExtractor: createCallExtractor(typescriptCallConfig),
|
||||
fieldExtractor: typescriptFieldExtractor,
|
||||
variableExtractor: createVariableExtractor(typescriptVariableConfig),
|
||||
classExtractor: vueClassExtractor,
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.TypeScript),
|
||||
builtInNames: VUE_BUILT_INS,
|
||||
// Scope-resolution pipeline hooks (RFC #909 Ring 3)
|
||||
emitScopeCaptures: emitVueScopeCaptures,
|
||||
|
|
|
|||
|
|
@ -176,6 +176,16 @@ export const javaMethodConfig: MethodExtractionConfig = {
|
|||
|
||||
const KOTLIN_VIS = new Set<MethodVisibility>(['public', 'private', 'protected', 'internal']);
|
||||
|
||||
function kotlinParameterHasDefaultValue(param: SyntaxNode): boolean {
|
||||
// tree-sitter-kotlin inlines `_function_value_parameter`, so `=` and its
|
||||
// expression are siblings of `parameter`, not children of the parameter.
|
||||
for (let sibling = param.nextSibling; sibling !== null; sibling = sibling.nextSibling) {
|
||||
if (sibling.type === ',' || sibling.type === ')') return false;
|
||||
if (sibling.type === '=') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractKotlinParameters(node: SyntaxNode): ParameterInfo[] {
|
||||
const params: ParameterInfo[] = [];
|
||||
// Kotlin: function_declaration > function_value_parameters > parameter
|
||||
|
|
@ -199,7 +209,6 @@ function extractKotlinParameters(node: SyntaxNode): ParameterInfo[] {
|
|||
let paramName: string | undefined;
|
||||
let paramType: string | null = null;
|
||||
let paramRawType: string | null = null;
|
||||
let hasDefault = false;
|
||||
const isVariadic = nextIsVariadic;
|
||||
nextIsVariadic = false;
|
||||
|
||||
|
|
@ -218,21 +227,12 @@ function extractKotlinParameters(node: SyntaxNode): ParameterInfo[] {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for default value: `= expr`
|
||||
for (let k = 0; k < param.childCount; k++) {
|
||||
const c = param.child(k);
|
||||
if (c && c.text === '=') {
|
||||
hasDefault = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (paramName) {
|
||||
params.push({
|
||||
name: paramName,
|
||||
type: paramType,
|
||||
rawType: paramRawType,
|
||||
isOptional: hasDefault,
|
||||
isOptional: kotlinParameterHasDefaultValue(param),
|
||||
isVariadic: isVariadic,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,418 +0,0 @@
|
|||
/**
|
||||
* Heritage Map
|
||||
*
|
||||
* Unified inheritance data structure built from accumulated
|
||||
* {@link ExtractedHeritage} records **after all chunks complete** (between
|
||||
* chunk processing and call resolution). Consumes `ExtractedHeritage[]` and
|
||||
* resolves type names to nodeIds via `lookupClassByName`, NOT graph-edge
|
||||
* queries.
|
||||
*
|
||||
* Combines two concerns:
|
||||
* 1. **Parent/ancestor lookup** (MRO-aware method resolution)
|
||||
* 2. **Implementor lookup** (interface dispatch — which files contain
|
||||
* classes implementing a given interface)
|
||||
*/
|
||||
|
||||
import type { ResolutionContext } from './resolution-context.js';
|
||||
import { getLanguageFromFilename, type SupportedLanguages } from 'gitnexus-shared';
|
||||
import {
|
||||
isDeferredResolutionProfileEnabled,
|
||||
logDeferredProfile,
|
||||
} from '../utils/deferred-resolution-profile.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ExtractedHeritage — the shape produced by the parse worker / heritage
|
||||
// extractor. Defined here so `model/` has no upward imports; consumers
|
||||
// import this type from the model module.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ExtractedHeritage {
|
||||
filePath: string;
|
||||
className: string;
|
||||
parentName: string;
|
||||
/** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */
|
||||
kind: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Heritage resolution strategy (the per-language knobs that drive
|
||||
// `resolveExtendsType` below). Pulled out as an explicit strategy object so
|
||||
// the model layer depends on a plain data shape rather than on the language
|
||||
// provider registry.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface HeritageResolutionStrategy {
|
||||
/** If set and the parent name matches, force IMPLEMENTS even when the
|
||||
* symbol is unresolved (e.g. `/^I[A-Z]/` for C# / Java). */
|
||||
readonly interfaceNamePattern?: RegExp;
|
||||
/** Fallback edge for unresolved parents when the name pattern doesn't
|
||||
* match (Swift uses 'IMPLEMENTS' for protocol conformance). */
|
||||
readonly defaultEdge: 'EXTENDS' | 'IMPLEMENTS';
|
||||
}
|
||||
|
||||
/** Callback used by `buildHeritageMap` to look up the resolution strategy
|
||||
* for a given language. Injected by callers so the model module doesn't
|
||||
* depend on `../languages/index.js`. */
|
||||
export type HeritageStrategyLookup = (lang: SupportedLanguages) => HeritageResolutionStrategy;
|
||||
|
||||
/**
|
||||
* Determine whether a heritage.extends capture is actually an IMPLEMENTS
|
||||
* relationship. Consults the symbol table first (authoritative — Tier 1 /
|
||||
* Tier 2 resolution); falls back to the injected {@link HeritageResolutionStrategy}
|
||||
* heuristics for external symbols not present in the graph.
|
||||
*/
|
||||
export const resolveExtendsType = (
|
||||
parentName: string,
|
||||
currentFilePath: string,
|
||||
ctx: ResolutionContext,
|
||||
strategy: HeritageResolutionStrategy,
|
||||
): { type: 'EXTENDS' | 'IMPLEMENTS'; idPrefix: string } => {
|
||||
const resolved = ctx.resolve(parentName, currentFilePath);
|
||||
if (resolved && resolved.candidates.length > 0) {
|
||||
const isInterface = resolved.candidates[0].type === 'Interface';
|
||||
return isInterface
|
||||
? { type: 'IMPLEMENTS', idPrefix: 'Interface' }
|
||||
: { type: 'EXTENDS', idPrefix: 'Class' };
|
||||
}
|
||||
// Unresolved symbol — fall back to strategy heuristics.
|
||||
if (strategy.interfaceNamePattern?.test(parentName)) {
|
||||
return { type: 'IMPLEMENTS', idPrefix: 'Interface' };
|
||||
}
|
||||
if (strategy.defaultEdge === 'IMPLEMENTS') {
|
||||
return { type: 'IMPLEMENTS', idPrefix: 'Interface' };
|
||||
}
|
||||
return { type: 'EXTENDS', idPrefix: 'Class' };
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Maximum ancestor chain depth to prevent runaway traversal. */
|
||||
const MAX_ANCESTOR_DEPTH = 32;
|
||||
|
||||
/**
|
||||
* Direct parent entry with the heritage kind that produced it. Preserved
|
||||
* so kind-aware consumers (Ruby MRO, see `lookupMethodByOwnerWithMRO`) can
|
||||
* walk prepend/include providers in the correct order. Flat-string consumers
|
||||
* use `getParents` / `getAncestors` and see only the parent nodeIds.
|
||||
*/
|
||||
export interface ParentEntry {
|
||||
readonly parentId: string;
|
||||
/** 'extends' | 'implements' | 'trait-impl' | 'include' | 'extend' | 'prepend' */
|
||||
readonly kind: string;
|
||||
}
|
||||
|
||||
export interface HeritageMap {
|
||||
/** Direct parents of `childNodeId` (extends + implements + trait-impl). */
|
||||
getParents(childNodeId: string): string[];
|
||||
/** Full ancestor chain (BFS, bounded depth, cycle-safe). */
|
||||
getAncestors(childNodeId: string): string[];
|
||||
/**
|
||||
* Direct parents with heritage kind preserved, insertion-ordered. Used by
|
||||
* kind-aware consumers (Ruby MRO) that need to distinguish prepend /
|
||||
* include / extend / extends for walk-order decisions.
|
||||
*
|
||||
* Insertion order mirrors the order `ExtractedHeritage` records were fed
|
||||
* into `buildHeritageMap`, which in turn mirrors tree-sitter match order.
|
||||
* For Ruby, this matches source declaration order for `prepend` / `include`
|
||||
* statements — the MRO walk reverses this (last-declared-first) at the
|
||||
* consumer side.
|
||||
*/
|
||||
getParentEntries(childNodeId: string): readonly ParentEntry[];
|
||||
/**
|
||||
* Ordered ancestry for instance method dispatch (Ruby-aware): includes
|
||||
* `extends`, `implements`, `trait-impl`, `include`, `prepend` kinds.
|
||||
* Excludes `extend` (singleton-only). Order is caller-determined in Unit 3.
|
||||
* For non-Ruby callers (first-wins, c3, etc.), this matches `getAncestors`.
|
||||
*/
|
||||
getInstanceAncestry(childNodeId: string): readonly ParentEntry[];
|
||||
/**
|
||||
* Ordered ancestry for singleton / class-method dispatch (Ruby-aware):
|
||||
* only `extend` kind parents. For non-Ruby languages this is always empty.
|
||||
*/
|
||||
getSingletonAncestry(childNodeId: string): readonly ParentEntry[];
|
||||
/**
|
||||
* File paths of classes that directly implement or extend-as-interface the
|
||||
* given interface/abstract-class **name**. Replaces the standalone
|
||||
* `ImplementorMap` — used by interface-dispatch in call resolution.
|
||||
*/
|
||||
getImplementorFiles(interfaceName: string): ReadonlySet<string>;
|
||||
}
|
||||
|
||||
/** Shared empty set returned when no implementors are found. */
|
||||
const EMPTY_SET: ReadonlySet<string> = new Set();
|
||||
|
||||
/** Default strategy used when `buildHeritageMap` is called without an
|
||||
* explicit `getHeritageStrategy` callback — the fallback for a language
|
||||
* whose provider sets no interface-name pattern and no non-default
|
||||
* `heritageDefaultEdge`. */
|
||||
const DEFAULT_HERITAGE_STRATEGY: HeritageResolutionStrategy = { defaultEdge: 'EXTENDS' };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a HeritageMap from accumulated ExtractedHeritage records.
|
||||
*
|
||||
* Resolves class/interface/struct/trait names to nodeIds via
|
||||
* `ctx.model.types.lookupClassByName`. When a name resolves to multiple
|
||||
* candidates, all are recorded (partial-class / cross-file scenario).
|
||||
* Unresolvable names are silently skipped — a missing parent is better
|
||||
* than a wrong edge.
|
||||
*
|
||||
* Also builds the implementor index (interface name → implementing file
|
||||
* paths) used by interface-dispatch in call resolution.
|
||||
*/
|
||||
export const buildHeritageMap = (
|
||||
heritage: readonly ExtractedHeritage[],
|
||||
ctx: ResolutionContext,
|
||||
getHeritageStrategy?: HeritageStrategyLookup,
|
||||
): HeritageMap => {
|
||||
// childNodeId → insertion-ordered array of { parentId, kind }.
|
||||
// Ordered array (not Set) because Ruby MRO walk depends on declaration
|
||||
// order. A parallel `seen` map dedupes `(parentId, kind)` pairs without
|
||||
// losing order.
|
||||
const directParents = new Map<string, ParentEntry[]>();
|
||||
const seenParents = new Map<string, Set<string>>();
|
||||
|
||||
// interfaceName → Set<filePath> (implementor lookup for interface dispatch)
|
||||
const implementorFiles = new Map<string, Set<string>>();
|
||||
|
||||
const profileHeritage = isDeferredResolutionProfileEnabled();
|
||||
let maxNameCartesian = 0;
|
||||
let ambiguousHeritageRecords = 0;
|
||||
let unresolvedChildLookups = 0;
|
||||
let unresolvedParentLookups = 0;
|
||||
|
||||
for (const h of heritage) {
|
||||
// ── Parent lookup (nodeId-based) ────────────────────────────────
|
||||
const childDefs = ctx.model.types.lookupClassByName(h.className);
|
||||
const parentDefs = ctx.model.types.lookupClassByName(h.parentName);
|
||||
|
||||
// Unresolved-side counters live in a separate guard so they observe
|
||||
// records the ambiguity block below skips. On JVM monorepos the
|
||||
// pathological fan-out case is precisely "many same-named children
|
||||
// with an unresolved external supertype" (or the inverse) — both
|
||||
// sides non-empty is the case `ambiguousHeritageRecords` already
|
||||
// covers; the unresolved cases were silently dropped from the
|
||||
// metric before this counter.
|
||||
if (profileHeritage) {
|
||||
if (childDefs.length === 0) unresolvedChildLookups++;
|
||||
if (parentDefs.length === 0) unresolvedParentLookups++;
|
||||
}
|
||||
|
||||
if (profileHeritage && childDefs.length > 0 && parentDefs.length > 0) {
|
||||
const product = childDefs.length * parentDefs.length;
|
||||
if (product > 1) ambiguousHeritageRecords++;
|
||||
if (product > maxNameCartesian) maxNameCartesian = product;
|
||||
}
|
||||
|
||||
if (childDefs.length > 0 && parentDefs.length > 0) {
|
||||
for (const child of childDefs) {
|
||||
for (const parent of parentDefs) {
|
||||
// Skip self-references
|
||||
if (child.nodeId === parent.nodeId) continue;
|
||||
|
||||
let parents = directParents.get(child.nodeId);
|
||||
if (!parents) {
|
||||
parents = [];
|
||||
directParents.set(child.nodeId, parents);
|
||||
}
|
||||
let seen = seenParents.get(child.nodeId);
|
||||
if (!seen) {
|
||||
seen = new Set();
|
||||
seenParents.set(child.nodeId, seen);
|
||||
}
|
||||
// Dedup by `parentId + kind` so the same parent under two different
|
||||
// kinds (e.g. a module that is both included and prepended — legal
|
||||
// Ruby though unusual) is recorded twice; the consumer needs both
|
||||
// kinds in the walk. A single (parent, kind) pair is deduped.
|
||||
const key = `${parent.nodeId}|${h.kind}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
parents.push({ parentId: parent.nodeId, kind: h.kind });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Implementor index (name-based) ──────────────────────────────
|
||||
//
|
||||
// Known limitation: Rust `kind: 'trait-impl'` entries are intentionally NOT
|
||||
// added to the implementor index. Interface dispatch resolution currently
|
||||
// does not traverse Rust trait objects, so recording them here would
|
||||
// inflate the index without a consumer. Revisit if/when trait-object
|
||||
// dispatch is added.
|
||||
//
|
||||
// Known limitation: `getImplementorFiles` is keyed by interface **name**
|
||||
// (string), so two interfaces with the same unqualified name in different
|
||||
// packages (e.g. `pkgA.IRepository` vs `pkgB.IRepository`) collide.
|
||||
let isImpl = false;
|
||||
if (h.kind === 'implements') {
|
||||
isImpl = true;
|
||||
} else if (h.kind === 'extends') {
|
||||
const lang = getLanguageFromFilename(h.filePath);
|
||||
if (lang) {
|
||||
const strategy = getHeritageStrategy?.(lang) ?? DEFAULT_HERITAGE_STRATEGY;
|
||||
const { type } = resolveExtendsType(h.parentName, h.filePath, ctx, strategy);
|
||||
isImpl = type === 'IMPLEMENTS';
|
||||
}
|
||||
}
|
||||
if (isImpl) {
|
||||
let files = implementorFiles.get(h.parentName);
|
||||
if (!files) {
|
||||
files = new Set();
|
||||
implementorFiles.set(h.parentName, files);
|
||||
}
|
||||
files.add(h.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Public API ---------------------------------------------------
|
||||
|
||||
/** Internal helper: return the entries array (may be undefined). */
|
||||
const entriesFor = (nodeId: string): readonly ParentEntry[] | undefined =>
|
||||
directParents.get(nodeId);
|
||||
|
||||
const getParentEntries = (childNodeId: string): readonly ParentEntry[] => {
|
||||
const entries = entriesFor(childNodeId);
|
||||
return entries ?? [];
|
||||
};
|
||||
|
||||
const getParents = (childNodeId: string): string[] => {
|
||||
const entries = entriesFor(childNodeId);
|
||||
if (!entries) return [];
|
||||
// Deduplicate parent ids across kinds so the flat-string contract
|
||||
// (used by non-Ruby MRO strategies and by the C3 linearizer) stays
|
||||
// identical to its pre-kind-awareness behavior.
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const e of entries) {
|
||||
if (!seen.has(e.parentId)) {
|
||||
seen.add(e.parentId);
|
||||
out.push(e.parentId);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const getAncestors = (childNodeId: string): string[] => {
|
||||
const result: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
visited.add(childNodeId); // prevent cycles through the start node
|
||||
|
||||
// BFS with bounded depth
|
||||
let frontier = getParents(childNodeId);
|
||||
let depth = 0;
|
||||
|
||||
while (frontier.length > 0 && depth < MAX_ANCESTOR_DEPTH) {
|
||||
const nextFrontier: string[] = [];
|
||||
for (const parentId of frontier) {
|
||||
if (visited.has(parentId)) continue;
|
||||
visited.add(parentId);
|
||||
result.push(parentId);
|
||||
// Expand parent's own parents for next level
|
||||
const grandparents = entriesFor(parentId);
|
||||
if (grandparents) {
|
||||
const gpSeen = new Set<string>();
|
||||
for (const gp of grandparents) {
|
||||
if (gpSeen.has(gp.parentId)) continue;
|
||||
gpSeen.add(gp.parentId);
|
||||
if (!visited.has(gp.parentId)) nextFrontier.push(gp.parentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier = nextFrontier;
|
||||
depth++;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazy-computed per-owner split of direct parents into instance-dispatch
|
||||
* (non-`extend`) and singleton-dispatch (`extend`-only) views. Memoized on
|
||||
* first request so the `.filter()` pass happens at most once per owner per
|
||||
* HeritageMap lifetime, not per call-site dispatch.
|
||||
*
|
||||
* Shared empty-array sentinels for owners with no entries in a given view
|
||||
* avoid per-call allocation when the split is asymmetric (common Ruby case:
|
||||
* a class has `include` but no `extend`, so its singleton view is empty).
|
||||
*/
|
||||
const EMPTY_PARENT_ENTRIES: readonly ParentEntry[] = [];
|
||||
const splitCache = new Map<
|
||||
string,
|
||||
{ instance: readonly ParentEntry[]; singleton: readonly ParentEntry[] }
|
||||
>();
|
||||
|
||||
const splitForOwner = (
|
||||
childNodeId: string,
|
||||
): { instance: readonly ParentEntry[]; singleton: readonly ParentEntry[] } => {
|
||||
let cached = splitCache.get(childNodeId);
|
||||
if (cached) return cached;
|
||||
const entries = entriesFor(childNodeId);
|
||||
if (!entries || entries.length === 0) {
|
||||
cached = { instance: EMPTY_PARENT_ENTRIES, singleton: EMPTY_PARENT_ENTRIES };
|
||||
} else {
|
||||
const instance: ParentEntry[] = [];
|
||||
const singleton: ParentEntry[] = [];
|
||||
for (const e of entries) {
|
||||
if (e.kind === 'extend') singleton.push(e);
|
||||
else instance.push(e);
|
||||
}
|
||||
cached = {
|
||||
instance: instance.length === 0 ? EMPTY_PARENT_ENTRIES : instance,
|
||||
singleton: singleton.length === 0 ? EMPTY_PARENT_ENTRIES : singleton,
|
||||
};
|
||||
}
|
||||
splitCache.set(childNodeId, cached);
|
||||
return cached;
|
||||
};
|
||||
|
||||
/**
|
||||
* Instance-dispatch ancestry walk. Excludes `extend` (singleton-only).
|
||||
* For kind-aware consumers (Ruby MRO): walks parents in source-insertion
|
||||
* order. The consumer is responsible for interleaving self / reversing
|
||||
* prepend order / etc. This method preserves raw declaration order.
|
||||
*
|
||||
* Result is cached per owner; repeat calls return the same array.
|
||||
*/
|
||||
const getInstanceAncestry = (childNodeId: string): readonly ParentEntry[] =>
|
||||
splitForOwner(childNodeId).instance;
|
||||
|
||||
/**
|
||||
* Singleton-dispatch ancestry walk. Only `extend` parents. For non-Ruby
|
||||
* languages this is always empty (no language currently produces `extend`
|
||||
* heritage records outside Ruby).
|
||||
*
|
||||
* Result is cached per owner; repeat calls return the same array.
|
||||
*/
|
||||
const getSingletonAncestry = (childNodeId: string): readonly ParentEntry[] =>
|
||||
splitForOwner(childNodeId).singleton;
|
||||
|
||||
const getImplementorFiles = (interfaceName: string): ReadonlySet<string> => {
|
||||
return implementorFiles.get(interfaceName) ?? EMPTY_SET;
|
||||
};
|
||||
|
||||
if (profileHeritage) {
|
||||
logDeferredProfile(
|
||||
`buildHeritageMap: ${heritage.length} heritage records, ` +
|
||||
`${ambiguousHeritageRecords} with child×parent lookup product >1, ` +
|
||||
`max product ${maxNameCartesian}, ` +
|
||||
`${unresolvedChildLookups} unresolved child lookups, ` +
|
||||
`${unresolvedParentLookups} unresolved parent lookups, ` +
|
||||
`${implementorFiles.size} interface implementor keys`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getParents,
|
||||
getAncestors,
|
||||
getParentEntries,
|
||||
getInstanceAncestry,
|
||||
getSingletonAncestry,
|
||||
getImplementorFiles,
|
||||
};
|
||||
};
|
||||
|
|
@ -59,30 +59,6 @@ export {
|
|||
createFieldRegistry,
|
||||
} from './field-registry.js';
|
||||
|
||||
// MRO-aware method resolution (C3, first-wins, leftmost-base, implements-split,
|
||||
// qualified-syntax). Pure function that depends only on the model + HeritageMap.
|
||||
// `MroStrategy` itself lives in `gitnexus-shared`; re-exported here for
|
||||
// consumers that reach model behavior through the barrel.
|
||||
export { lookupMethodByOwnerWithMRO } from './resolve.js';
|
||||
|
||||
// Named-import types and package-dir helper. Re-exported so barrel
|
||||
// consumers don't need to reach into a specific model file.
|
||||
export {
|
||||
type NamedImportBinding,
|
||||
type NamedImportMap,
|
||||
isFileInPackageDir,
|
||||
} from './resolution-context.js';
|
||||
|
||||
// Heritage types and builder. `buildHeritageMap` + `resolveExtendsType` are
|
||||
// exported directly from `heritage-map.ts` and are not re-surfaced here to
|
||||
// keep the barrel narrow.
|
||||
export {
|
||||
type ExtractedHeritage,
|
||||
type HeritageMap,
|
||||
type HeritageResolutionStrategy,
|
||||
type HeritageStrategyLookup,
|
||||
} from './heritage-map.js';
|
||||
|
||||
// Behavior-grouped dispatch table for SymbolTable.add() routing.
|
||||
// See registration-table.ts module JSDoc for the behavior group taxonomy
|
||||
// and "how to add a new NodeLabel" checklist.
|
||||
|
|
|
|||
|
|
@ -1,455 +0,0 @@
|
|||
/**
|
||||
* Resolution Context
|
||||
*
|
||||
* Single implementation of tiered name resolution.
|
||||
*
|
||||
* Resolution tiers (highest confidence first):
|
||||
* 1. Same file (lookupExactAll — authoritative)
|
||||
* 2a-named. Named binding chain (walkBindingChain via NamedImportMap)
|
||||
* 2a. Import-scoped (iterate importedFiles with lookupExactAll per file)
|
||||
* 2b. Package-scoped (iterate indexed files matching package dir with lookupExactAll)
|
||||
* 3. Global (lookupClassByName + lookupImplByName + lookupCallableByName — consumers must check count)
|
||||
*
|
||||
* Each tier queries the minimum necessary scope directly:
|
||||
* - Tier 2a iterates the caller's import set (O(imports) × O(1) lookupExactAll).
|
||||
* - Tier 2b iterates all indexed files filtered by package dir
|
||||
* (O(files) × O(1) lookupExactAll — avoids a global name scan).
|
||||
* - Tier 3 combines lookupClassByName + lookupImplByName + lookupCallableByName
|
||||
* (three O(1) index lookups with a narrow, type-specific result set).
|
||||
*/
|
||||
|
||||
import type { SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { SymbolTableReader } from './symbol-table.js';
|
||||
import type { MutableSemanticModel } from './semantic-model.js';
|
||||
import { createSemanticModel } from './semantic-model.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Named-import types — describe how a file imports specific names from a
|
||||
// source file. Consumed by the Tier 2a-named binding-chain walker below.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A single named binding in a source file (e.g. `import { User as U }`).
|
||||
* Stores both the resolved source path and the original exported name so
|
||||
* that aliased imports can resolve U → User in the source file.
|
||||
*/
|
||||
export interface NamedImportBinding {
|
||||
sourcePath: string;
|
||||
exportedName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map<ImportingFilePath, Map<LocalName, NamedImportBinding>>.
|
||||
*
|
||||
* Tracks which specific names a file imports from which sources (TS / Python
|
||||
* / Rust / Java-static / ...). Used to tighten Tier 2a resolution:
|
||||
* `import { User } from './models'` means only `User` (not `Repo`) is
|
||||
* visible from models.ts via this import.
|
||||
*/
|
||||
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
|
||||
|
||||
/**
|
||||
* Check if a file path is directly inside a package directory identified by
|
||||
* its suffix. Used by Tier 2b package-scoped resolution (Go / C#).
|
||||
*/
|
||||
export function isFileInPackageDir(filePath: string, dirSuffix: string): boolean {
|
||||
// Prepend '/' so paths like "internal/auth/service.go" match suffix "/internal/auth/"
|
||||
const normalized = '/' + filePath.replace(/\\/g, '/');
|
||||
if (!normalized.includes(dirSuffix)) return false;
|
||||
const afterDir = normalized.substring(normalized.indexOf(dirSuffix) + dirSuffix.length);
|
||||
return !afterDir.includes('/');
|
||||
}
|
||||
|
||||
/** Maximum re-export hops walkBindingChain will follow before giving up.
|
||||
* A hard cap is needed to defend against pathological cycles that slip
|
||||
* past the `visited` Set (e.g. a binding chain whose key is equal by
|
||||
* string value but visits distinct modules). Five hops covers the
|
||||
* common TypeScript monorepo pattern (component → pkg/index →
|
||||
* packages/index → root/index → types/index). Chains longer than this
|
||||
* fall through to Tier 2a-import / Tier 2b / Tier 3 resolution, which
|
||||
* is a silent false-negative that the caller may or may not recover
|
||||
* from. If a real repo hits this limit, raise it — there is no
|
||||
* correctness reason to keep it at exactly 5. */
|
||||
const MAX_BINDING_CHAIN_DEPTH = 5;
|
||||
|
||||
/**
|
||||
* Walk a named-binding re-export chain through NamedImportMap.
|
||||
*
|
||||
* When file A imports { User } from B, and B re-exports { User } from C,
|
||||
* the NamedImportMap for A points to B, but B has no User definition.
|
||||
* This function follows the chain: A → B → C until a definition is found.
|
||||
*
|
||||
* Returns the definitions found at the end of the chain, or null if the
|
||||
* chain breaks (missing binding, circular reference, or
|
||||
* {@link MAX_BINDING_CHAIN_DEPTH} exceeded). Internal to
|
||||
* resolution-context — not exported from the model barrel.
|
||||
*/
|
||||
function walkBindingChain(
|
||||
name: string,
|
||||
currentFilePath: string,
|
||||
symbolTable: SymbolTableReader,
|
||||
namedImportMap: NamedImportMap,
|
||||
): readonly SymbolDefinition[] | null {
|
||||
// Fast exit: most files have no named imports at all. Skip the Set
|
||||
// allocation + loop entry on the common empty-binding path so resolve()
|
||||
// stays allocation-free for the typical call site.
|
||||
const firstBindings = namedImportMap.get(currentFilePath);
|
||||
if (!firstBindings) return null;
|
||||
const firstBinding = firstBindings.get(name);
|
||||
if (!firstBinding) return null;
|
||||
|
||||
let lookupFile = currentFilePath;
|
||||
let lookupName = name;
|
||||
const visited = new Set<string>();
|
||||
|
||||
for (let depth = 0; depth < MAX_BINDING_CHAIN_DEPTH; depth++) {
|
||||
const bindings = depth === 0 ? firstBindings : namedImportMap.get(lookupFile);
|
||||
if (!bindings) return null;
|
||||
|
||||
const binding = depth === 0 ? firstBinding : bindings.get(lookupName);
|
||||
if (!binding) return null;
|
||||
|
||||
const key = `${binding.sourcePath}:${binding.exportedName}`;
|
||||
if (visited.has(key)) return null; // circular
|
||||
visited.add(key);
|
||||
|
||||
const targetName = binding.exportedName;
|
||||
const resolvedDefs = symbolTable.lookupExactAll(binding.sourcePath, targetName);
|
||||
|
||||
if (resolvedDefs.length > 0) return resolvedDefs;
|
||||
|
||||
// No definition in source file → follow re-export chain
|
||||
lookupFile = binding.sourcePath;
|
||||
lookupName = targetName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolution tier for tracking, logging, and test assertions. */
|
||||
export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
|
||||
|
||||
/** Tier-selected candidates with metadata. */
|
||||
export interface TieredCandidates {
|
||||
readonly candidates: readonly SymbolDefinition[];
|
||||
readonly tier: ResolutionTier;
|
||||
}
|
||||
|
||||
/** Confidence scores per resolution tier. */
|
||||
export const TIER_CONFIDENCE: Record<ResolutionTier, number> = {
|
||||
'same-file': 0.95,
|
||||
'import-scoped': 0.9,
|
||||
global: 0.5,
|
||||
};
|
||||
|
||||
// --- Map types ---
|
||||
export type ImportMap = Map<string, Set<string>>;
|
||||
export type PackageMap = Map<string, Set<string>>;
|
||||
/** Maps callerFile → (moduleAlias → sourceFilePath) for Python namespace imports.
|
||||
* e.g. `import models` in app.py → moduleAliasMap.get('app.py')?.get('models') === 'models.py' */
|
||||
export type ModuleAliasMap = Map<string, Map<string, string>>;
|
||||
|
||||
export interface ResolutionContext {
|
||||
/**
|
||||
* The only resolution API. Returns all candidates at the winning tier.
|
||||
*
|
||||
* Tier 3 ('global') returns ALL candidates regardless of count —
|
||||
* consumers must check candidates.length and refuse ambiguous matches.
|
||||
*/
|
||||
resolve(name: string, fromFile: string): TieredCandidates | null;
|
||||
|
||||
// --- Data access (for pipeline wiring, not resolution) ---
|
||||
/** Semantic model — the top-level container for types, methods, fields,
|
||||
* and the nested file/callable SymbolTable. Typed as
|
||||
* {@link MutableSemanticModel} because `ResolutionContext` is the
|
||||
* lifecycle owner — the pipeline registers symbols through it during
|
||||
* the fan-out phase. Resolvers that only query should annotate their
|
||||
* own fields as {@link SemanticModel} to drop write access. */
|
||||
readonly model: MutableSemanticModel;
|
||||
/** Raw maps — used by import-processor to populate import data. */
|
||||
readonly importMap: ImportMap;
|
||||
readonly packageMap: PackageMap;
|
||||
readonly namedImportMap: NamedImportMap;
|
||||
/** Module-alias map for Python namespace imports: callerFile → (alias → sourceFile). */
|
||||
readonly moduleAliasMap: ModuleAliasMap;
|
||||
|
||||
// --- Per-file cache lifecycle ---
|
||||
enableCache(filePath: string): void;
|
||||
clearCache(): void;
|
||||
|
||||
// --- Operational ---
|
||||
getStats(): {
|
||||
fileCount: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
tierSameFile: number;
|
||||
tierImportScoped: number;
|
||||
tierGlobal: number;
|
||||
tierMiss: number;
|
||||
};
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export const createResolutionContext = (): ResolutionContext => {
|
||||
const model = createSemanticModel();
|
||||
const symbols = model.symbols;
|
||||
const importMap: ImportMap = new Map();
|
||||
const packageMap: PackageMap = new Map();
|
||||
const namedImportMap: NamedImportMap = new Map();
|
||||
const moduleAliasMap: ModuleAliasMap = new Map();
|
||||
|
||||
// Inverted index: packageDirSuffix → Set<filePath>.
|
||||
// Built lazily on first Tier 2b hit — one-time cost of O(totalFiles ×
|
||||
// allUniqueDirSuffixes) isFileInPackageDir calls across the entire
|
||||
// packageMap, amortized over the pipeline run. Subsequent Tier 2b
|
||||
// resolutions are O(callerPackages × filesInPackage × O(1)).
|
||||
let packageDirIndex: Map<string, Set<string>> | null = null;
|
||||
|
||||
// Per-file cache state
|
||||
let cacheFile: string | null = null;
|
||||
let cache: Map<string, TieredCandidates | null> | null = null;
|
||||
let cacheHits = 0;
|
||||
let cacheMisses = 0;
|
||||
// Tier hit counters — replaces the lost fuzzyCallCount diagnostic
|
||||
let tierSameFile = 0;
|
||||
let tierImportScoped = 0;
|
||||
let tierGlobal = 0;
|
||||
let tierMiss = 0;
|
||||
|
||||
// --- Core resolution (single implementation of tier logic) ---
|
||||
|
||||
const resolveUncached = (name: string, fromFile: string): TieredCandidates | null => {
|
||||
// Tier 1: Same file — authoritative match (returns all overloads)
|
||||
const localDefs = symbols.lookupExactAll(fromFile, name);
|
||||
if (localDefs.length > 0) {
|
||||
tierSameFile++;
|
||||
return { candidates: localDefs, tier: 'same-file' };
|
||||
}
|
||||
|
||||
// Tier 2a-named: Named binding chain (aliased / re-exported imports)
|
||||
// Checked before import-scoped so that `import { User as U }` resolves
|
||||
// correctly even when lookupExactAll on the alias name returns nothing.
|
||||
const chainResult = walkBindingChain(name, fromFile, symbols, namedImportMap);
|
||||
if (chainResult && chainResult.length > 0) {
|
||||
tierImportScoped++;
|
||||
return { candidates: chainResult, tier: 'import-scoped' };
|
||||
}
|
||||
|
||||
// Tier 2a: Import-scoped — iterate the caller's imported files directly.
|
||||
// O(importedFiles) × O(1) lookupExactAll — no global name scan needed.
|
||||
const importedFiles = importMap.get(fromFile);
|
||||
if (importedFiles) {
|
||||
const importedDefs: SymbolDefinition[] = [];
|
||||
for (const file of importedFiles) {
|
||||
importedDefs.push(...symbols.lookupExactAll(file, name));
|
||||
}
|
||||
if (importedDefs.length > 0) {
|
||||
tierImportScoped++;
|
||||
return { candidates: importedDefs, tier: 'import-scoped' };
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 2b: Package-scoped — look up files in the caller's imported package
|
||||
// directories via an inverted index (packageDirSuffix → Set<filePath>),
|
||||
// then do O(1) lookupExactAll per file. The inverted index is built lazily
|
||||
// on first Tier 2b hit by scanning symbols.getFiles() once, making
|
||||
// subsequent Tier 2b resolutions O(packages × filesInPackage) instead of
|
||||
// O(allFiles × packages).
|
||||
const importedPackages = packageMap.get(fromFile);
|
||||
if (importedPackages) {
|
||||
// Lazily build the inverted index on first use. For each indexed file,
|
||||
// test it against isFileInPackageDir for all known dirSuffixes collected
|
||||
// from packageMap. This scans all files once (instead of per-resolution)
|
||||
// and produces a dirSuffix → Set<filePath> map.
|
||||
if (!packageDirIndex) {
|
||||
// Collect all unique dir suffixes across the entire packageMap
|
||||
const allDirSuffixes = new Set<string>();
|
||||
for (const dirs of packageMap.values()) {
|
||||
for (const d of dirs) allDirSuffixes.add(d);
|
||||
}
|
||||
packageDirIndex = new Map();
|
||||
for (const file of symbols.getFiles()) {
|
||||
for (const dirSuffix of allDirSuffixes) {
|
||||
if (isFileInPackageDir(file, dirSuffix)) {
|
||||
let files = packageDirIndex.get(dirSuffix);
|
||||
if (!files) {
|
||||
files = new Set();
|
||||
packageDirIndex.set(dirSuffix, files);
|
||||
}
|
||||
files.add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const packageDefs: SymbolDefinition[] = [];
|
||||
for (const dirSuffix of importedPackages) {
|
||||
const filesInDir = packageDirIndex.get(dirSuffix);
|
||||
if (filesInDir) {
|
||||
for (const file of filesInDir) {
|
||||
packageDefs.push(...symbols.lookupExactAll(file, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (packageDefs.length > 0) {
|
||||
tierImportScoped++;
|
||||
return { candidates: packageDefs, tier: 'import-scoped' };
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: Global — targeted O(1) index lookups for each symbol category.
|
||||
// Class-like symbols (Class, Struct, Interface, Enum, Record, Trait) are
|
||||
// covered by lookupClassByName; Rust impl blocks by lookupImplByName
|
||||
// (separate to avoid polluting heritage resolution); free callables
|
||||
// (Function, Macro, Delegate) by lookupCallableByName; owner-scoped
|
||||
// methods and constructors by `model.methods.lookupMethodByName`.
|
||||
//
|
||||
// FREE_CALLABLE_TYPES excludes Method/Constructor, so strictly-labeled
|
||||
// methods are disjoint between the two indexes.
|
||||
//
|
||||
// Partial-state caveat: Python/Rust/Kotlin class methods are emitted
|
||||
// as Function + ownerId — `rawSymbols.add` routes them through both
|
||||
// the Function callable index AND, via the dispatch-key normalization
|
||||
// in `wrappedAdd`, the method registry. The same `SymbolDefinition`
|
||||
// reference lands in both `callableDefs` and `methodDefs`, so the
|
||||
// Set-based dedup below is required.
|
||||
//
|
||||
// Known exclusion: TypeAlias, Const, and Variable are NOT reachable at
|
||||
// Tier 3 — they don't belong to any of the indexes. TypeAlias is not
|
||||
// a call target; Const/Variable are resolved via import or same-file
|
||||
// tiers. Macro (C/C++) and Delegate (C#) stay in the callable index
|
||||
// since call-processor.ts treats them as callable targets.
|
||||
const classDefs = model.types.lookupClassByName(name);
|
||||
const implDefs = model.types.lookupImplByName(name);
|
||||
const callableDefs = symbols.lookupCallableByName(name);
|
||||
const methodDefs = model.methods.lookupMethodByName(name);
|
||||
|
||||
if (
|
||||
classDefs.length === 0 &&
|
||||
implDefs.length === 0 &&
|
||||
callableDefs.length === 0 &&
|
||||
methodDefs.length === 0
|
||||
) {
|
||||
tierMiss++;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fast path: if no `Function + ownerId` class method was ever
|
||||
// registered into the method registry (the only source of
|
||||
// cross-index duplication), the callable and method indexes are
|
||||
// guaranteed disjoint and we can concat without dedup.
|
||||
if (!model.methods.hasFunctionMethods) {
|
||||
const globalDefs: SymbolDefinition[] = [
|
||||
...classDefs,
|
||||
...implDefs,
|
||||
...callableDefs,
|
||||
...methodDefs,
|
||||
];
|
||||
tierGlobal++;
|
||||
return { candidates: globalDefs, tier: 'global' };
|
||||
}
|
||||
|
||||
// Slow path: dedup by nodeId because the same SymbolDefinition
|
||||
// reference can land in both `callableDefs` (via the Function
|
||||
// callable-index gate) and `methodDefs` (via the dispatch-key
|
||||
// normalization routing Function+ownerId into MethodRegistry).
|
||||
// Dedup covers all four index reads so any nodeId overlap (even
|
||||
// theoretical ones between classDefs/implDefs) is caught.
|
||||
const globalDefs: SymbolDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
const pushUnique = (pool: readonly SymbolDefinition[]): void => {
|
||||
for (const def of pool) {
|
||||
if (seen.has(def.nodeId)) continue;
|
||||
seen.add(def.nodeId);
|
||||
globalDefs.push(def);
|
||||
}
|
||||
};
|
||||
pushUnique(classDefs);
|
||||
pushUnique(implDefs);
|
||||
pushUnique(callableDefs);
|
||||
pushUnique(methodDefs);
|
||||
|
||||
tierGlobal++;
|
||||
return { candidates: globalDefs, tier: 'global' };
|
||||
};
|
||||
|
||||
const resolve = (name: string, fromFile: string): TieredCandidates | null => {
|
||||
// Check cache (only when enabled AND fromFile matches cached file)
|
||||
if (cache && cacheFile === fromFile) {
|
||||
if (cache.has(name)) {
|
||||
cacheHits++;
|
||||
return cache.get(name)!;
|
||||
}
|
||||
cacheMisses++;
|
||||
}
|
||||
|
||||
const result = resolveUncached(name, fromFile);
|
||||
|
||||
// Store in cache if active and file matches
|
||||
if (cache && cacheFile === fromFile) {
|
||||
cache.set(name, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// --- Cache lifecycle ---
|
||||
|
||||
const enableCache = (filePath: string): void => {
|
||||
cacheFile = filePath;
|
||||
if (!cache) cache = new Map();
|
||||
else cache.clear();
|
||||
};
|
||||
|
||||
const clearCache = (): void => {
|
||||
cacheFile = null;
|
||||
// Reuse the Map instance — just clear entries to reduce GC pressure at scale.
|
||||
cache?.clear();
|
||||
// Note: packageDirIndex is NOT invalidated here. It is built lazily on
|
||||
// first Tier 2b hit and remains valid across file boundaries because
|
||||
// packageMap and the symbol file set are append-only during the calls
|
||||
// phase (all parsing/import processing completes before resolution).
|
||||
// Invalidating per-file would destroy the amortization benefit — the
|
||||
// O(files × dirs) rebuild would run per-file instead of once.
|
||||
// Full invalidation happens in clear() (pipeline reset).
|
||||
};
|
||||
|
||||
const getStats = () => ({
|
||||
...symbols.getStats(),
|
||||
cacheHits,
|
||||
cacheMisses,
|
||||
tierSameFile,
|
||||
tierImportScoped,
|
||||
tierGlobal,
|
||||
tierMiss,
|
||||
});
|
||||
|
||||
const clear = (): void => {
|
||||
model.clear();
|
||||
importMap.clear();
|
||||
packageMap.clear();
|
||||
namedImportMap.clear();
|
||||
moduleAliasMap.clear();
|
||||
packageDirIndex = null; // invalidate — will rebuild on next Tier 2b hit
|
||||
clearCache();
|
||||
cacheHits = 0;
|
||||
cacheMisses = 0;
|
||||
tierSameFile = 0;
|
||||
tierImportScoped = 0;
|
||||
tierGlobal = 0;
|
||||
tierMiss = 0;
|
||||
};
|
||||
|
||||
return {
|
||||
resolve,
|
||||
model,
|
||||
importMap,
|
||||
packageMap,
|
||||
namedImportMap,
|
||||
moduleAliasMap,
|
||||
enableCache,
|
||||
clearCache,
|
||||
getStats,
|
||||
clear,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,30 +1,18 @@
|
|||
/**
|
||||
* Deterministic Resolution Functions
|
||||
* MRO primitives.
|
||||
*
|
||||
* Pure functions that resolve methods across the inheritance hierarchy
|
||||
* using only the SemanticModel registries and HeritageMap — NO dependency
|
||||
* on resolution-context.ts (circular dependency risk).
|
||||
* `c3Linearize` and its BFS helper `gatherAncestors` are pure functions over a
|
||||
* `parentMap` (classId → parent ids). They carry no dependency on the semantic
|
||||
* model or graph, so the model layer stays a pure leaf — `mro-processor.ts`
|
||||
* (graph-level MRO emission) imports `c3Linearize` / `gatherAncestors` from here.
|
||||
*/
|
||||
|
||||
import type { SymbolDefinition } from 'gitnexus-shared';
|
||||
import type { SemanticModel } from './semantic-model.js';
|
||||
import type { HeritageMap } from './heritage-map.js';
|
||||
import type { MroStrategy } from 'gitnexus-shared';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MRO primitives.
|
||||
//
|
||||
// `c3Linearize` and its BFS helper `gatherAncestors` live here so the model
|
||||
// layer stays a pure leaf — mro-processor.ts (graph-level MRO emission)
|
||||
// imports `c3Linearize` from this file.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gather all ancestor IDs in BFS / topological order.
|
||||
* Returns the linearized list of ancestor IDs (excluding the class itself).
|
||||
*
|
||||
* Uses a head-pointer BFS (`queue[head++]`) instead of `Array.shift()` to
|
||||
* avoid O(n) per-dequeue re-indexing — matching `buildParentMapFromHeritage`.
|
||||
* avoid O(n) per-dequeue re-indexing.
|
||||
*/
|
||||
function gatherAncestors(classId: string, parentMap: Map<string, string[]>): string[] {
|
||||
const visited = new Set<string>();
|
||||
|
|
@ -53,8 +41,7 @@ function gatherAncestors(classId: string, parentMap: Map<string, string[]>): str
|
|||
* Returns an array of ancestor IDs in C3 order (excluding the class itself),
|
||||
* or null if linearization fails (inconsistent or cyclic hierarchy).
|
||||
*
|
||||
* Used internally by `lookupMethodByOwnerWithMRO` for the Python MRO
|
||||
* strategy and re-exported for mro-processor.ts (graph-level MRO emission).
|
||||
* Re-exported for mro-processor.ts (graph-level MRO emission).
|
||||
*/
|
||||
export function c3Linearize(
|
||||
classId: string,
|
||||
|
|
@ -217,231 +204,3 @@ export function c3Linearize(
|
|||
// `gatherAncestors` is exported so mro-processor.ts can reuse the same
|
||||
// BFS traversal for graph-level MRO emission.
|
||||
export { gatherAncestors };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C3 linearization cache (per HeritageMap, auto-drained via WeakMap)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Per-HeritageMap cache of C3 linearization results keyed by owner nodeId.
|
||||
*
|
||||
* HeritageMap instances are immutable after construction, so C3 output is
|
||||
* stable for the lifetime of a HeritageMap. WeakMap lets the cache auto-drain
|
||||
* when the HeritageMap is garbage collected (end of ingestion run), so we
|
||||
* never need to manually invalidate it.
|
||||
*
|
||||
* `null` is a sentinel for "C3 failed for this owner" (cyclic or inconsistent
|
||||
* hierarchy) so we don't re-run the expensive linearization repeatedly.
|
||||
*/
|
||||
const c3LinearizationCache = new WeakMap<HeritageMap, Map<string, readonly string[] | null>>();
|
||||
|
||||
const getCachedC3Linearization = (
|
||||
ownerNodeId: string,
|
||||
heritageMap: HeritageMap,
|
||||
): readonly string[] | null => {
|
||||
let perHmCache = c3LinearizationCache.get(heritageMap);
|
||||
if (!perHmCache) {
|
||||
perHmCache = new Map();
|
||||
c3LinearizationCache.set(heritageMap, perHmCache);
|
||||
}
|
||||
const cached = perHmCache.get(ownerNodeId);
|
||||
if (cached !== undefined) return cached;
|
||||
const parentMap = buildParentMapFromHeritage(ownerNodeId, heritageMap);
|
||||
const result = c3Linearize(ownerNodeId, parentMap, new Map()) ?? null;
|
||||
perHmCache.set(ownerNodeId, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Heritage → parentMap conversion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a parentMap from HeritageMap for use with c3Linearize.
|
||||
* Traverses the parent chain starting from startNodeId, collecting all
|
||||
* parent→children relationships into a Map<string, string[]>.
|
||||
*
|
||||
* Uses a head-pointer BFS (queue[head++]) instead of Array.shift() to avoid
|
||||
* O(n) per-dequeue re-indexing. For wide/shallow hierarchies common in
|
||||
* large Java/C# codebases this keeps the walk linear in ancestor count.
|
||||
*/
|
||||
const buildParentMapFromHeritage = (
|
||||
startNodeId: string,
|
||||
heritageMap: HeritageMap,
|
||||
): Map<string, string[]> => {
|
||||
const parentMap = new Map<string, string[]>();
|
||||
const visited = new Set<string>();
|
||||
const queue: string[] = [startNodeId];
|
||||
let head = 0;
|
||||
|
||||
while (head < queue.length) {
|
||||
const nodeId = queue[head++]!;
|
||||
if (visited.has(nodeId)) continue;
|
||||
visited.add(nodeId);
|
||||
const parents = heritageMap.getParents(nodeId);
|
||||
if (parents.length > 0) {
|
||||
parentMap.set(nodeId, parents);
|
||||
for (const p of parents) {
|
||||
if (!visited.has(p)) queue.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parentMap;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MRO-aware method lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* DAG stage 5 helper: look up a method on an owner class via MRO walk.
|
||||
*
|
||||
* Low-level resolver; no dependency on SymbolTable, language registry, or
|
||||
* resolution-context (keeps model/ layer free of cross-layer imports).
|
||||
* All strategies respect `argCount` for overload narrowing.
|
||||
* `ancestryOverride` replaces the default walk; caller must compute it correctly.
|
||||
*
|
||||
* Strategy summary (full docs in gitnexus-shared/mro-strategy.ts):
|
||||
* - `first-wins` / `leftmost-base` / `implements-split`: BFS, first match wins.
|
||||
* - `c3`: C3-linearized order; falls back to BFS on cycle/inconsistency.
|
||||
* - `qualified-syntax`: returns undefined immediately (Rust requires explicit syntax).
|
||||
* - `ruby-mixin`: kind-aware walk — see inline comments below.
|
||||
*
|
||||
* Internal API: exported for call-processor resolvers and tests.
|
||||
* External callers should use resolveMemberCall instead.
|
||||
*
|
||||
* @see gitnexus-shared/mro-strategy.ts § 'ruby-mixin'
|
||||
* @see call-processor.ts § resolveMemberCall
|
||||
*/
|
||||
export const lookupMethodByOwnerWithMRO = (
|
||||
ownerNodeId: string,
|
||||
methodName: string,
|
||||
heritageMap: HeritageMap,
|
||||
model: SemanticModel,
|
||||
strategy: MroStrategy,
|
||||
argCount?: number,
|
||||
/**
|
||||
* Optional pre-computed ancestry list. When provided, overrides the default
|
||||
* per-strategy ancestry source. Primarily used by Ruby singleton dispatch:
|
||||
* the caller supplies `heritageMap.getSingletonAncestry(ownerNodeId)` as
|
||||
* node-id array so this walker resolves against `extend` providers only.
|
||||
*
|
||||
* For `ruby-mixin` strategy, passing an override switches the walker into
|
||||
* a no-prepend-no-direct linear scan (the caller has already decided the
|
||||
* order), which is the correct semantics for singleton dispatch.
|
||||
*/
|
||||
ancestryOverride?: readonly string[],
|
||||
): SymbolDefinition | undefined => {
|
||||
// ── Ruby mixin strategy ───────────────────────────────────────────
|
||||
// Kind-aware walk — does NOT short-circuit on direct owner first (prepend beats direct).
|
||||
// Instance dispatch: prepend (reverse) → direct → include (reverse) → transitive BFS.
|
||||
// Singleton dispatch: caller supplies ancestryOverride (extend providers only);
|
||||
// simple left-to-right scan. Miss NEVER falls through to file-scoped fallback.
|
||||
// See gitnexus-shared/mro-strategy.ts § 'ruby-mixin' for full strategy docs.
|
||||
if (strategy === 'ruby-mixin') {
|
||||
if (ancestryOverride) {
|
||||
// Singleton dispatch: scan pre-computed ancestry only. Miss null-routes.
|
||||
for (const ancestorId of ancestryOverride) {
|
||||
const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
|
||||
if (method) return method;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Instance dispatch — kind-aware walk per the pseudocode above.
|
||||
const instanceEntries = heritageMap.getInstanceAncestry(ownerNodeId);
|
||||
// Partition into prepend parents vs other parents (extends / include /
|
||||
// implements / trait-impl), preserving declaration order within each.
|
||||
const prependParents: string[] = [];
|
||||
const otherParents: string[] = [];
|
||||
for (const e of instanceEntries) {
|
||||
if (e.kind === 'prepend') prependParents.push(e.parentId);
|
||||
else otherParents.push(e.parentId);
|
||||
}
|
||||
|
||||
// Step 1: Walk prepend parents in REVERSE declaration order (last-prepended wins).
|
||||
for (let i = prependParents.length - 1; i >= 0; i--) {
|
||||
const method = model.methods.lookupMethodByOwner(prependParents[i], methodName, argCount);
|
||||
if (method) return method;
|
||||
}
|
||||
|
||||
// Step 2: Direct owner lookup (the class's own method).
|
||||
// This is the only difference from other strategies — prepend beats direct.
|
||||
const direct = model.methods.lookupMethodByOwner(ownerNodeId, methodName, argCount);
|
||||
if (direct) return direct;
|
||||
|
||||
// Step 3: Walk extends + include parents in REVERSE declaration order.
|
||||
// (Ruby `include A; include B` puts B ahead of A in MRO.)
|
||||
for (let i = otherParents.length - 1; i >= 0; i--) {
|
||||
const method = model.methods.lookupMethodByOwner(otherParents[i], methodName, argCount);
|
||||
if (method) return method;
|
||||
}
|
||||
|
||||
// Step 4: Transitive ancestors (a mixin that itself mixes in another module).
|
||||
// Fall back to the BFS ancestor walk for depth > 1. Order is best-effort;
|
||||
// Ruby's actual MRO for transitive mixins is rare and under-specified
|
||||
// (documented in architecture docs as deferred work).
|
||||
//
|
||||
// O(1) skip-check via Sets:
|
||||
// - `walkedDirect` covers parents already visited in steps 1-3.
|
||||
// - `singletonOnly` covers direct `extend` providers: they belong to
|
||||
// the singleton MRO and must NEVER appear in instance dispatch.
|
||||
// Building Sets once before the BFS loop avoids O(n²) `Array.includes`
|
||||
// on large mixin hierarchies.
|
||||
const walkedDirect = new Set<string>(prependParents);
|
||||
for (const id of otherParents) walkedDirect.add(id);
|
||||
const singletonOnly = new Set<string>(
|
||||
heritageMap.getSingletonAncestry(ownerNodeId).map((e) => e.parentId),
|
||||
);
|
||||
for (const ancestorId of heritageMap.getAncestors(ownerNodeId)) {
|
||||
if (ancestorId === ownerNodeId) continue;
|
||||
if (walkedDirect.has(ancestorId)) continue;
|
||||
if (singletonOnly.has(ancestorId)) continue;
|
||||
const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
|
||||
if (method) return method;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── Non-Ruby strategies: direct-owner-first short-circuit ─────────
|
||||
|
||||
// Direct lookup first (child override — no walk needed).
|
||||
// argCount is threaded through so arity-differing overloads on the direct
|
||||
// owner can be disambiguated before the MRO walk starts.
|
||||
const direct = model.methods.lookupMethodByOwner(ownerNodeId, methodName, argCount);
|
||||
if (direct) return direct;
|
||||
|
||||
// Rust: requires qualified syntax (<Type as Trait>::method), no auto-resolution
|
||||
if (strategy === 'qualified-syntax') return undefined;
|
||||
|
||||
// Determine ancestor walk order based on MRO strategy.
|
||||
// readonly to accept the cached (frozen) c3 linearization without copying.
|
||||
let ancestors: readonly string[];
|
||||
if (ancestryOverride) {
|
||||
ancestors = ancestryOverride;
|
||||
} else if (strategy === 'c3') {
|
||||
// C3 linearization (memoized per HeritageMap
|
||||
// so repeated calls for the same owner within an ingestion run reuse the
|
||||
// linearization instead of rebuilding the parent map and re-running C3).
|
||||
// c3Linearize returns ancestors only (excludes the owner itself),
|
||||
// matching heritageMap.getAncestors() semantics.
|
||||
const c3Result = getCachedC3Linearization(ownerNodeId, heritageMap);
|
||||
// Fall back to BFS order if C3 fails (cyclic or inconsistent hierarchy).
|
||||
// Note: BFS order may not preserve Python MRO semantics in these edge
|
||||
// cases, but cyclic/inconsistent hierarchies are invalid in Python anyway.
|
||||
ancestors = c3Result ?? heritageMap.getAncestors(ownerNodeId);
|
||||
} else {
|
||||
// first-wins, leftmost-base, implements-split: BFS order via HeritageMap
|
||||
ancestors = heritageMap.getAncestors(ownerNodeId);
|
||||
}
|
||||
|
||||
// Walk ancestors in MRO order — first match wins.
|
||||
// argCount narrows overloaded ancestors the same way as the direct lookup.
|
||||
for (const ancestorId of ancestors) {
|
||||
const method = model.methods.lookupMethodByOwner(ancestorId, methodName, argCount);
|
||||
if (method) return method;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue