Commit graph

715 commits

Author SHA1 Message Date
evolution
2b7cff5fd2
feat(embeddings): structural chunking with data-driven CHUNKING_RULES dispatch (#987)
* feat(embeddings): structural chunking with data-driven CHUNKING_RULES dispatch

Replace hardcoded label comparisons with a CHUNKING_RULES lookup table
that drives chunking strategy and text generation. Key changes:

- Data-driven dispatch: CHUNKING_RULES table maps labels to chunking
  mode (ast-function / ast-declaration), prefix/suffix, field grouping,
  and structural text mode
- Struct support: add Struct to AST declaration chunking with field
  grouping (same as Class)
- Multi-chunk context: preceding chunk tail (prevTail) injected into
  embedding text for cross-chunk coherence
- Version-gated hashes: EMBEDDING_TEXT_VERSION prefix in content hashes
  invalidates stale vectors when text template changes
- Compact container context: first declaration line preserved in every
  structural chunk for identity

* fix(embeddings): address PR review findings for CHUNKING_RULES refactor

- Remove LABEL_ENUM from STRUCTURAL_LABELS to avoid wasted AST parses
- Add maintenance note about extractStructuralNames and EMBEDDING_TEXT_VERSION
- Clarify CHUNK_MODE_CHARACTER is a no-op in CHUNKING_RULES
- Strengthen EMBEDDING_TEXT_VERSION test assertion to exact value

---------

Co-authored-by: wangjichao <wangjichao@inke.cn>
2026-04-20 08:25:31 +01:00
Copilot
d976038dc8
fix: guard RC docker job against empty vtag and add early validation in docker.yml (#983)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* Initial plan

* fix: guard docker job and add tag validation in docker.yml

- Add `&& needs.publish.outputs.vtag != ''` to the `docker` job's
  `if:` in release-candidate.yml so it is skipped when publish
  produces no vtag, preventing an opaque buildx "tag is needed" error.
- Add an early "Validate tag input" step in docker.yml that fails fast
  with a clear ::error:: message when inputs.tag is empty, covering
  direct workflow_call invocations that bypass the release-candidate
  guard.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b9afe2df-85ea-4a87-bf30-77f0e945a64d

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: scope docker.yml tag validation to workflow_call only

Direct tag-push triggers (on: push, tags: v*) populate the tag from
GITHUB_REF and have inputs.tag empty, so the unconditional validation
step would fail every direct tag-push run. Restrict the new step to
workflow_call invocations, which is the only path where an empty tag
is actually a problem.

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/4b7e3bfa-15c0-4186-affa-95cd71e50153

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-19 09:41:30 +01:00
Copilot
9926804d75
feat(cli): infer registry name from git remote.origin.url (#981)
* Initial plan

* Plan: smarter index name inference via git remote URL

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/95064d2d-b1da-4c89-9069-5b3e9cc2636a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* feat(cli): infer registry name from git remote.origin.url (#979)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/95064d2d-b1da-4c89-9069-5b3e9cc2636a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor: skip git subprocess when --name was supplied (review feedback)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/95064d2d-b1da-4c89-9069-5b3e9cc2636a

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style: prettier --write on run-analyze.ts

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a4bf631d-ea6b-4d84-b426-29b1e5c3539f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-19 09:14:14 +01:00
Copilot
fa39a4b4a5
fix(docker): build and push Docker images for Release Candidates (#978) 2026-04-19 07:46:21 +01:00
azizur100389
dae7bd3b3f
feat(cli): analyze --name <alias> + duplicate-name guard for the repo registry (#955) 2026-04-19 07:23:48 +01:00
Ryanba
363245eb63
fix: detect React component paths before lowercasing (#260) 2026-04-19 07:10:36 +01:00
Gergő Magyar
6222b5be9b
feat(ingestion): emit-references drains ReferenceIndex to graph edges (#925, RFC #909 Ring 2 PKG) (#973)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
2026-04-18 23:36:10 +01:00
Gergő Magyar
e2ba4a04c9
feat(ingestion): shadow-mode parity harness + static dashboard (#923, RFC #909 Ring 2 PKG) (#972)
* feat(ingestion): shadow-mode parity harness + static dashboard (#923, RFC #909 Ring 2 PKG)

Side-car observability for the RFC #909 registry rollout. Callers that
dual-run legacy-DAG + `Registry.lookup` feed their result pairs into
the harness; the harness diffs each pair via shared `diffResolutions`
(#918), aggregates via `aggregateDiffs`, and persists a per-language
parity report that the static dashboard can render offline.

## Shipped

### `gitnexus/src/core/ingestion/shadow-harness.ts` (new)

```ts
createShadowHarness(): ShadowHarness
```

API:
  - `enabled` — `true` iff `GITNEXUS_SHADOW_MODE` is truthy at
    construction. Captured once; later env-var mutations don't flip it.
  - `record({ language, callsite, legacy, newResult, primary })` —
    accumulator. No-op when `enabled === false` (near-zero overhead).
  - `size()` — diagnostic counter.
  - `snapshot(now?)` — deterministic `ShadowParityReport` from the
    accumulated diffs.
  - `persist(outputDir, now?)` — writes BOTH a timestamped
    `<runId>.json` and a `latest.json` pointer. Creates outputDir if
    absent. Returns the per-run file path.
  - `clear()` — resets the accumulator; preserves `enabled`.

Activation: `GITNEXUS_SHADOW_MODE` accepts `'true'` / `'1'` / `'yes'`
(case-insensitive, trimmed); same truthy convention as
`REGISTRY_PRIMARY_<LANG>` from #924. Typos → disabled (fail-safe).

Persisted payload (`PersistedShadowReport`) is schema-versioned (`v1`):

```jsonc
{
  "schemaVersion": 1,
  "runId": "YYYYMMDD-HHMMSS-xxxxxxxx",
  "generatedAt": "ISO 8601",
  "primaryByLanguage": { "python": "legacy", ... },
  "report": { /* ShadowParityReport from #918 aggregateDiffs */ }
}
```

`runId` prefix is the timestamp so files sort chronologically; the
entropy suffix prevents collisions within a clock-second.

### `gitnexus/shadow-parity-dashboard/index.html` (new)

Minimal static dashboard — one HTML file, zero build step, zero runtime
deps. Fetches `./latest.json` and renders:

  - Overall summary cards (total calls, both agree, disagree, overall parity %)
  - Per-language table: language tag ("primary: legacy" / "primary:
    registry" pill) + total / agree / only-legacy / only-new / disagree
    / both-empty / parity%
  - Parity cells colored by threshold: ≥95% green, ≥80% amber, <80% red
  - Light / dark via `prefers-color-scheme`
  - Empty-state message when no records yet

File-serving is static: `cp .gitnexus/shadow-parity/latest.json
gitnexus/shadow-parity-dashboard/` + open in a browser.

## Tests (14, all passing)

  - **Flag detection** (5): default off · truthy variants case-insensitive ·
    falsy / typo → off · record() is no-op when disabled · env flip
    AFTER construction doesn't enable (constructed-once semantics)
  - **Record + snapshot** (4): multi-language accumulation ·
    per-language rows with correct outcomes · snapshot determinism ·
    `clear()` resets accumulator + `primaryByLanguage`
  - **Persistence** (5): mkdir-p on missing outputDir · per-run +
    latest.json match byte-for-byte · schema v1 payload shape ·
    runId timestamp prefix sorts chronologically · empty report
    persists gracefully

Tests use a per-test tmpdir (`fs.mkdtemp`), cleaned in `afterEach`,
so parallel vitest runs don't collide. `GITNEXUS_SHADOW_MODE` is
saved + restored per-test.

## What's deliberately NOT in this PR (call-out in harness docstring)

  - **Dual-run dispatch.** The harness is a side-car — it does NOT
    invoke either resolution path. Call-processor integration that
    actually runs both legacy + registry paths lands as a follow-up.
    Without that integration, `record()` is never called in production
    today. The harness is tested in isolation with synthetic inputs.
  - **CI artifact publishing.** Config work to upload
    `latest.json` + the dashboard HTML per CI run. Tracked separately;
    the harness + dashboard are ready when the CI job wires in.
  - **Fixture-level drill-down.** The issue mentions per-fixture AST
    snippet + evidence trace drill-down. MVP dashboard shows per-language
    rows only; drill-down extends the static JSON format + the dashboard
    JS in a focused follow-up.

## Verification

  - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`)
  - 14/14 new tests pass
  - Full scope-resolution / shadow / model / flag suite: **335/335 pass**

## Part of

  - Parent: #909
  - Depends on (code): #917 (registries), #918 (diff + aggregate)
  - Unblocks Ring 3 language flips: the parity dashboard becomes the
    checkpoint before flipping `REGISTRY_PRIMARY_<LANG>=true` for a
    language — once per-language parity stabilizes, the flip ships.

* chore: prettier format on shadow-parity-dashboard index.html
2026-04-18 21:30:43 +01:00
Gergő Magyar
0c37eda482
feat(ingestion): per-language resolveImportTarget adapter (#922, RFC #909 Ring 2 PKG) (#971)
Bridges the CLI's existing per-language `ImportResolverFn`s (16 languages
already implemented) to the shared `FinalizeHooks.resolveImportTarget`
contract consumed by `finalize()` (#915) and
`finalizeScopeModel` (#921).

No resolver logic is reimplemented — the adapter wraps
`provider.importResolver` from each `LanguageProvider` verbatim.

## Shipped

### `import-target-adapter.ts` (new)

```ts
buildImportTargetWorkspace(providers, resolveCtx): ImportTargetWorkspace
resolveImportTargetAcrossLanguages(targetRaw, fromFile, workspaceIndex): string | null
```

  - `ImportTargetWorkspace` is the opaque `workspaceIndex` shape the
    adapter recognizes: `{ perLanguage: Map<SupportedLanguages,
    { resolver, ctx }> }`. Callers build it once per ingestion run from
    the active language providers.
  - `resolveImportTargetAcrossLanguages` is the `FinalizeHook`
    implementation. It:
      1. Reads `getLanguageFromFilename(fromFile)`.
      2. Looks up the per-language entry.
      3. Calls the existing `ImportResolverFn` — same signature, same
         code path the legacy DAG uses today.
      4. Picks `result.files[0]` (covers both `'files'` and `'package'`
         result kinds; the legacy pipeline's richer multi-file + dirSuffix
         semantics stay accessible through `importResolver` directly).
      5. Returns `null` on any null result, empty files[], unknown
         extension, missing workspace, or resolver exception.
  - Exceptions from resolvers are swallowed — the finalize algorithm
    treats `null` as `linkStatus: 'unresolved'`, which is the right
    fallback for malformed inputs.

### What's deliberately NOT here

  - **Re-implementation of any per-language resolver.** Wraps the
    existing `importResolver` field on each provider.
  - **Dynamic-import handling.** The shared finalize algorithm short-
    circuits `ParsedImport { kind: 'dynamic-unresolved' }` before
    calling `resolveImportTarget`, so the adapter never sees them.
  - **`importPathPreprocessor`.** Preprocessing belongs inside the
    provider's `interpretImport` hook that produces
    `ParsedImport.targetRaw`; the adapter forwards that verbatim.

## Tests (12, all passing)

  - **`buildImportTargetWorkspace`** (3): registers providers with
    importResolver · skips providers without · threads shared ctx
    into every entry
  - **`resolveImportTargetAcrossLanguages`** (9): forwards targetRaw +
    fromFile · dispatches by extension · null resolver result →
    null · `package`-kind takes first file · empty files[] → null ·
    no registered resolver → null · unknown extension → null ·
    undefined/malformed workspace → null · resolver throw → null

Real per-language resolver correctness is covered by the existing
per-language resolver test suites — the adapter is the bridge layer.

## Verification

  - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`)
  - `gitnexus-shared` build clean
  - 12/12 new tests pass
  - Full scope-resolution / shadow / model / flag suite: **333/333 pass**

## Integration flow

```ts
const workspace = buildImportTargetWorkspace(providers, resolveCtx);
const indexes = finalizeScopeModel(parsedFiles, {
  hooks: { resolveImportTarget: resolveImportTargetAcrossLanguages },
  workspaceIndex: workspace,
});
model.attachScopeIndexes(indexes);
```

## Closes part of #909. Unblocks

  - Ring 3 language migrations (#926+): a language flipping to
    `REGISTRY_PRIMARY_<LANG>=true` now has correct import-target
    resolution out of the box via its existing `importResolver`.
  - #923 shadow harness — can run the dual-path comparison knowing
    both sides use the same per-language resolution semantics.
2026-04-18 21:11:24 +01:00
Copilot
3adb97e993
feat(docker): ship signed UI + CLI/server images via docker-compose (#967)
* Initial plan

* docker: ship signed UI + CLI/server images via docker-compose

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/883bcee1-4a1d-4b3d-bbb9-accd8846da96

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker: lock image version to npm package + harden cosign verify guidance

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6afd4fcd-5656-4e02-b796-a22b59000bde

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker: add Sigstore ClusterImagePolicy + k8s admission docs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/aea3dd70-e2a9-443a-b578-cb3eca4093e1

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker(k8s): collapse redundant image globs in ClusterImagePolicy

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/aea3dd70-e2a9-443a-b578-cb3eca4093e1

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker(ci): drop deprecated COSIGN_EXPERIMENTAL, dead build-args, and loose verify regex in comment

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bdf0d2cf-607c-4558-982a-be9b216b2d36

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docker(ci): use ${{ github.repository }} in verify-comment regex for fork portability

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bdf0d2cf-607c-4558-982a-be9b216b2d36

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style(deploy): prettier-format cluster-image-policy.yaml (single quotes)

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b09a016e-56a1-4b73-bacc-e69084a48782

* ci(docker): drop workflow_dispatch, harden signing loop, fix verify-comment placeholder

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/6755064c-7871-4b2e-9b46-b4779eb215ac

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-18 20:55:19 +01:00
Gergő Magyar
25520e90a5
feat(ingestion): finalize-orchestrator materializes ScopeResolutionIndexes (#921, RFC #909 Ring 2 PKG) (#970)
Ties the Ring 2 pipeline together. Takes the `ParsedFile[]` produced by
#920's parse-worker integration, feeds them to shared `finalize()`
(#915), and bundles every workspace-wide index for attachment onto
`MutableSemanticModel`. Thin integration glue per issue #884's boundary
— all algorithm lives in `gitnexus-shared`.

## Shipped

### `model/scope-resolution-indexes.ts` (new)

```ts
interface ScopeResolutionIndexes {
  readonly scopeTree: ScopeTree;
  readonly defs: DefIndex;
  readonly qualifiedNames: QualifiedNameIndex;
  readonly moduleScopes: ModuleScopeIndex;
  readonly methodDispatch: MethodDispatchIndex;
  readonly imports: ReadonlyMap<ScopeId, readonly ImportEdge[]>;
  readonly bindings: ReadonlyMap<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>;
  readonly referenceSites: readonly ReferenceSite[];
  readonly sccs: readonly FinalizedScc[];
  readonly stats: FinalizeStats;
}
```

The bundle produced by the orchestrator, consumed by the resolution
phase. `ReferenceIndex` is deliberately NOT here — it's populated in
the next phase (#925).

### `model/semantic-model.ts` — extended

  - `SemanticModel.scopes?: ScopeResolutionIndexes` — undefined until
    attached; once attached, frozen.
  - `MutableSemanticModel.attachScopeIndexes(indexes)` — one-shot write.
    Throws on second call; `Object.freeze`s the bundle on write. `clear()`
    resets the slot back to `undefined` so re-ingestion can re-attach.

### `finalize-orchestrator.ts` (new)

```ts
finalizeScopeModel(parsedFiles, options?): ScopeResolutionIndexes
```

Orchestration steps:

  1. Map `ParsedFile[]` → `FinalizeInput` (`FinalizeFile` is a structural
     subset, so no shape-shifting).
  2. Call shared `finalize()` with provider hooks (defaults provided for
     the zero-provider case today).
  3. Build the four workspace indexes (`DefIndex`, `QualifiedNameIndex`,
     `ModuleScopeIndex`, `ScopeTree`) from per-file unions.
  4. Build an empty `MethodDispatchIndex` as a placeholder (owners=[],
     both callbacks return []). Real MRO wiring lands with the
     per-language adapters in #922.
  5. Bundle + return.

**Empty-input safety.** Zero parsedFiles → valid but empty bundle with
all zero-sized indexes and `stats.totalFiles === 0`. Downstream code
can consult `model.scopes` without branching on presence — only on
`stats`.

**Hook defaults** (`withDefaultHooks`) for missing provider hooks:

  - `resolveImportTarget: () => null` — every import goes `unresolved`
  - `expandsWildcardTo: () => []` — wildcards don't materialize
  - `mergeBindings: (a, b) => [...a, ...b]` — append without precedence

Providers override these in #922 (per-language import adapters).

## Tests (10, all passing)

  - **Empty input** (1): zero parsedFiles → valid empty bundle
  - **Single file** (2): all per-file indexes populated · referenceSites
    aggregated
  - **Cross-file imports** (3): resolveImportTarget threads through +
    links · default-null resolver → unresolved · stats reflect graph
  - **MutableSemanticModel integration** (4): undefined initially · attach
    once · Object.freeze applied · throws on re-attach · clear() resets

## Verification

  - `tsc --noEmit` clean in both packages
  - `gitnexus-shared` build clean
  - 10/10 new tests pass
  - Full scope-resolution / shadow / model / flag suite: **321/321 pass**

## What's deferred (not this PR, per RFC #909 scope)

  - **Per-language hook adapters** (#922): `resolveImportTarget` +
    `expandsWildcardTo` + `mergeBindings` wired per language.
  - **MethodDispatchIndex wiring via HeritageMap**: populate MRO + implements
    via the existing CLI-package HeritageMap strategies. Likely companion
    to #922 or a focused follow-up.
  - **Pipeline invocation**: actually calling `finalizeScopeModel` from
    the real ingestion pipeline. The orchestrator is callable today; the
    ingestion entry point wiring lands with the shadow harness (#923).
  - **`ReferenceIndex` population**: RFC §3.2 Phase 4 / #925.

## Closes part of #909. Unblocks
  - #923 shadow harness — now has a fully materialized `model.scopes` to
    query against the legacy DAG for parity measurement
  - #925 ReferenceIndex → LadybugDB emission — consumes `model.scopes`
  - Ring 3 language migrations (#926+) — a language flipping to
    `REGISTRY_PRIMARY_<LANG>=true` can now expect `model.scopes` to be
    populated when the pipeline wires the orchestrator in
2026-04-18 20:51:19 +01:00
Gergő Magyar
39b5d295c7
feat(ingestion): wire ScopeExtractor into parse-worker + processor (#920, RFC #909 Ring 2 PKG) (#969)
Plumbs the ScopeExtractor (#919) into the real parsing pipeline.
`ParsedFile` artifacts now flow from workers to the parsing-processor
without changing any legacy-DAG behavior.

## Shipped

### `gitnexus/src/core/ingestion/scope-extractor-bridge.ts` (new)

  - `extractParsedFile(provider, sourceText, filePath, onWarn?)`
  - Short-circuits (returns `undefined`) when the provider has not
    implemented `emitScopeCaptures`. True for every language today —
    this is the default no-op path.
  - Invokes the hook + `ScopeExtractor.extract`, returns a `ParsedFile`.
  - **Swallows exceptions on both sides.** Failures route through the
    optional `onWarn` callback (or `console.warn`) and return
    `undefined`. Scope-extraction errors NEVER break legacy parsing on
    the same file.
  - Standalone module (not nested in `parse-worker.ts`) so tests can
    import it directly without triggering the worker's top-level
    `parentPort!.on(...)`.

### `gitnexus/src/core/ingestion/workers/parse-worker.ts`

  - `ParseWorkerResult.parsedFiles: ParsedFile[]` added.
  - `processFileGroup` calls `extractParsedFile` AFTER tree parse,
    BEFORE legacy extraction. Worker provides an `onWarn` callback that
    routes bridge warnings through `parentPort.postMessage({ type:
    'warning', message })`.
  - `mergeResult` includes `parsedFiles` in the sub-batch merge.
  - Initial + reset accumulator templates include `parsedFiles: []`.

### `gitnexus/src/core/ingestion/parsing-processor.ts`

  - `WorkerExtractedData.parsedFiles: ParsedFile[]` added.
  - Empty-result branch and the across-chunk aggregation both include
    `parsedFiles`. Aggregation is tolerant of workers that don't emit
    the field (older builds / partial rollouts).

### Ring 1 tweak: `emitScopeCaptures` sync return

`readonly CaptureMatch[]` (was `Promise<readonly CaptureMatch[]>`).
Tree-sitter and COBOL's regex tagger are both synchronous; no
foreseeable need for async work inside this hook. Sync lets the
already-sync worker pipeline invoke it inline without cascading
`async` up through the batch driver + IPC handler.

## Tests (9 new; full suite 311/311)

`gitnexus/test/unit/scope-resolution/parse-worker-scope-integration.test.ts`:
  - Not-migrated (2): undefined-returning hook · never-invokes-extractor
  - Migrated (3): happy path · argument threading · honors
    `shouldCreateScope` override
  - Error resilience (4): hook throws · extractor throws (no Module) ·
    extractor throws (sibling overlap) · `onWarn` gets routed
    message with filePath + error body

## Verification

  - `tsc --noEmit` clean in both packages
  - `gitnexus-shared` build clean
  - 311/311 combined scope-resolution / shadow / model / flag suite
  - 9/9 new bridge tests

## What's NOT in this PR (still deferred to #921)

  - Actually using the `parsedFiles` — that's the finalize orchestrator.
  - `ModuleScopeIndex.byFilePath` materialization — belongs alongside
    the rest of the SemanticModel indexes in #921.

## Closes part of #909. Unblocks
  - #921 finalize-orchestrator — consumes `WorkerExtractedData.parsedFiles`
2026-04-18 20:27:56 +01:00
Gergő Magyar
eece6344fc
feat(ingestion): REGISTRY_PRIMARY_<LANG> per-language flag reader (#924, RFC #909 Ring 2 PKG) (#968)
Adds the per-language feature flag primitive that gates the Ring 3
registry-primary rollout. Single source of truth for whether a given
language uses `Registry.lookup` (new) or the legacy DAG (current).

## Shipped

### `gitnexus/src/core/ingestion/registry-primary-flag.ts`

  - `isRegistryPrimary(lang): boolean` — reads
    `REGISTRY_PRIMARY_<UPPER(enum-value)>` from `process.env`.
  - `envVarNameFor(lang): string` — exposed for CI tooling that
    cross-references flag flips (and for test assertions).
  - `primaryLanguages(): ReadonlySet<SupportedLanguages>` — all
    currently-on languages; useful for startup logging + the #923
    shadow dashboard which distinguishes "primary: legacy" vs
    "primary: registry" rows.

### Contract

  - Default: `false` for every language. A language must explicitly
    opt in by setting its env var.
  - Truthy: `'true'`, `'1'`, `'yes'` (case-insensitive, whitespace-
    trimmed). Anything else — typos, empty string, `'off'` — is
    `false`. Fail-safe posture: a misspelled flag doesn't accidentally
    flip a language.
  - No per-process caching. `process.env` is read per call; overhead
    is negligible (one lookup per file at resolution time), and
    test isolation is lexical (no cache-reset coordination).

### Env-var mapping

Uses the enum VALUE, not the TS key, for the env-var suffix:

  - `SupportedLanguages.Python`     → `REGISTRY_PRIMARY_PYTHON`
  - `SupportedLanguages.CPlusPlus`  → `REGISTRY_PRIMARY_CPP`   (value `'cpp'`)
  - `SupportedLanguages.CSharp`     → `REGISTRY_PRIMARY_CSHARP`

Users flip languages by their canonical name, not the TS symbol.

## Tests (16, all passing)

  - `envVarNameFor` (3): upper-casing · enum-VALUE-not-KEY mapping ·
    all-languages uniqueness smoke-test
  - `isRegistryPrimary` (9): default false · `'true'` / `'1'` / `'yes'`
    truthy · mixed-case + whitespace-padded · falsy-looking values ·
    unrecognized tokens (typo-safe) · per-language isolation · no
    stale cache on mid-process mutation · CPlusPlus mapping
  - `primaryLanguages` (3): empty · exact membership · Set instanceof

Tests scrub every `REGISTRY_PRIMARY_*` env var in `beforeEach` +
`afterEach` so parallel vitest runs on the same process don't bleed state.

## What's NOT in this PR (deferred by design)

The actual integration in `call-processor.ts` belongs in #921
(finalize-orchestrator). Reason: the "new path" requires a populated
`SemanticModel` to call `Registry.lookup` against, and the model
becomes accessible only after #921 orchestrates finalize. Wiring a
dead branch now would just get rewritten then.

This PR ships the flag primitive in isolation so #921 has a clean,
tested utility to consult — and so `#923` (shadow harness) has a
stable boolean to read for its "which row is primary?" rendering.

## Closes part of #909. Unblocks
  - #921 finalize-orchestrator — can now consult `isRegistryPrimary`
    at resolution time
  - #923 shadow harness — can distinguish primary-flipped rows
2026-04-18 19:54:54 +01:00
Gergő Magyar
c6a291de67
feat(ingestion): ScopeExtractor driver — 5-pass CaptureMatch → ParsedFile (#919, RFC #909 Ring 2 PKG) (#965)
* feat(ingestion): ScopeExtractor driver — 5-pass CaptureMatch → ParsedFile (#919, RFC #909 Ring 2 PKG)

Kicks off Ring 2 PKG. Implements RFC §5.3 + §3.2 Phase 1: the central,
source-agnostic driver that turns a language provider's `CaptureMatch[]`
into a `ParsedFile` — the per-file artifact the finalize orchestrator
(#921) feeds into the shared `finalize()` algorithm (#915).

## Files

### New shared contracts
  - `gitnexus-shared/src/scope-resolution/parsed-file.ts`
    Per-file extraction artifact: scopes, parsedImports, localDefs,
    referenceSites. Structural superset of `FinalizeFile` so the
    finalize orchestrator threads `ParsedFile` through unchanged.
  - `gitnexus-shared/src/scope-resolution/reference-site.ts`
    Pre-resolution usage fact: name, atRange, inScope, kind, optional
    callForm/explicitReceiver/arity. Converted to `Reference` records
    by the resolution phase (populates `ReferenceIndex`).

### Ring 1 collateral tweak
  - `language-provider.ts: emitScopeCaptures` now returns
    `Promise<readonly CaptureMatch[]>` (was `readonly Capture[]`).
    Pre-grouping per tree-sitter match is the provider's job — the
    extractor expects coherent matches, not flat captures. No
    consumers yet (all languages still on legacy DAG), so no breakage.
    Docstring updated.

### New CLI module
  - `gitnexus/src/core/ingestion/scope-extractor.ts`
    Single entry point: `extract(matches, filePath, provider): ParsedFile`.
    Five-pass pipeline:

      Pass 1 — Build scope tree. `@scope.*` → `ScopeDraft[]` via
        range-containment parent derivation. Honors
        `provider.shouldCreateScope` (skip-but-reparent-children) and
        `provider.resolveScopeKind`. Throws `ScopeTreeInvariantError`
        via `buildScopeTree` on malformed input.

      Pass 2 — Attach declarations + local bindings. `@declaration.*`
        → `SymbolDefinition` + `BindingRef { origin: 'local' }`.
        Default attachment: innermost containing scope. Hoisting via
        `provider.bindingScopeFor`.

      Pass 3 — Collect raw imports. `@import.*` → `ParsedImport` via
        `provider.interpretImport`. Attached to ParsedFile
        (finalize resolves owning scope in Phase 2).

      Pass 4 — Collect type bindings. `@type-binding.*` →
        `TypeRef` via `provider.interpretTypeBinding` →
        `scope.typeBindings`. Hoistable via `bindingScopeFor`.

      Pass 5 — Collect reference sites. `@reference.*` →
        `ReferenceSite[]`. Call form from declarative sub-tag
        (`@reference.call.member`) or `provider.classifyCallForm`.

### Tests
  - `gitnexus/test/unit/scope-resolution/scope-extractor.test.ts`
    23 tests organized by pass + one end-to-end fixture exercising
    all 5 passes together. MockProvider emits synthetic
    `CaptureMatch[]` with no AST — extractor is pure given those.

## Design notes

- **Source-agnostic.** No `Tree` / `SyntaxNode` types leak into the
  driver. Works for tree-sitter providers and COBOL's regex tagger.
- **One AST walk per language.** Providers do the walk inside
  `emitScopeCaptures`; this driver does zero traversal.
- **Invariants delegated.** `ScopeTree.buildScopeTree` enforces
  structural rules (non-Module has parent, parent contains child,
  siblings don't overlap). The extractor doesn't try to repair
  malformed captures.
- **Sub-tag whitelist.** `@reference.receiver`, `@declaration.name`,
  `@import.source`, etc. are known sub-tags — excluded from anchor
  selection so the broadest-range heuristic doesn't mis-identify them
  as anchors for their topic. Bug surfaced in the end-to-end fixture
  test (member call with a large-range receiver) and was fixed before
  commit.

## Verification

  - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`)
  - `gitnexus-shared` build clean
  - 23/23 new tests pass
  - Full scope-resolution / model / shadow suite: **285/285 pass**

## Closes part of #909. Unblocks
  - #920 parse-worker integration (emit ParsedFile from the worker)
  - #921 finalize orchestrator (consume ParsedFile[] workspace-wide)
  - #922 per-language import adapters

* chore(ingestion): address #919 review findings on the extractor

Addresses all 5 items from the PR #965 review in-PR.

## Structural changes

- **Extract `ScopeExtractorHooks` as the narrow dependency surface.**
  The extractor now declares its dependency on a `Pick`-narrowed subset
  of `LanguageProvider` (just the 6 scope-resolution hooks it actually
  reads). Test mocks implement exactly that interface — no more
  `as unknown as LanguageProvider` cast hiding missing-field bugs.
  Adding a new hook read becomes a compile error, not a silent test
  pass. (Finding 3.2)

- **Remove dead `ownerDefIdFor` stub + `isOwnerKind` helper.** The
  function always returned `undefined` with `void innermost; void
  drafts;` suppressors — an incomplete-implementation signal. The code
  path was also misleading: creating a clone of the def with
  `ownerId: undefined` is structurally identical to keeping the
  original. Pass 2 now keeps the def as-is. Contract is documented in
  a code comment: providers that need `ownerId` set it from their
  declaration hook; `finalize` (via #914 `MethodDispatchIndex`) fills
  in method/field `ownerId` in a post-extraction pass that has full
  def visibility. (Finding 2.1)

- **Standardize `filePath` threading across passes 4 and 5.** Pass 4
  was reading `drafts[0]!.filePath`; pass 5 was reading
  `anyFilePathFromScopeTree(scopeTree)`. Both equivalent but
  inconsistent. Both now take `filePath` as a parameter from the
  top-level `extract()` call. The `anyFilePathFromScopeTree` helper is
  removed. (Finding 2.2)

## Documentation

- **Snapshot-semantics comment on `scopeTree` + `positionIndex`.** The
  hooks called during Passes 2-5 receive a `scopeTree` built BEFORE any
  bindings/ownedDefs/typeBindings were written. Hooks MUST NOT rely on
  `scope.bindings` etc. being populated — they're for parent/range/kind
  queries only. Added a doc block at the `scopeTree`/`positionIndex`
  construction site so future Ring 3 implementers don't write a
  `classifyCallForm` that reads bindings. (Finding 2.3)

## Tests

- **Regression for the anchor-vs-receiver bug** (Finding 3.1): a
  member-call match where `@reference.receiver` spans columns 0-10
  (wider) and the call name spans 11-15 (narrower). Without the
  `KNOWN_SUB_TAGS` exclusion, the broadest-range heuristic would have
  picked the receiver; the test pins that the call name is the one
  that ends up in `referenceSites[0].name`.

- **Mock provider now types exactly `ScopeExtractorHooks`**, no more
  double-cast. Any future hook added to `extract()` that isn't in
  `ScopeExtractorHooks` is a compile error.

## Verification

- `tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus`
- `gitnexus-shared` build clean
- 24/24 scope-extractor tests pass (+1 regression)
- Full scope-resolution / model / shadow suite: **286/286 pass**
2026-04-18 19:28:51 +01:00
Gergő Magyar
e944f90879
chore(shared): apply Ring 2 SHARED review follow-ups in one diff (#964)
* chore(shared): apply Ring 2 SHARED review follow-ups in one diff

Aggregates all actionable follow-ups from the 9 Ring 2 SHARED PRs
(#949–#963) before proceeding to Ring 2 PKG. No behavior changes;
docstring edits, test refinements, and one structural cleanup.

## #913 (DefIndex / ModuleScopeIndex / QualifiedNameIndex)
  - Rename `freezeIndex` → `wrapIndex` across all three index builders.
    The old name implied `Object.freeze` on the wrapper, which we never
    applied; `wrapIndex` more accurately describes the lightweight
    readonly-interface wrap. Safety surface (frozen bucket arrays,
    frozen miss-empty array, readonly Maps) is unchanged.
  - Document in `buildModuleScopeIndex` JSDoc that callers must
    pre-normalize `filePath` keys (no path-separator canonicalization
    happens here). Prevents silent cross-platform misses.
  - Add an explicit hit-path freeze assertion in
    `qualified-name-index.test.ts` (the existing test covered only the
    miss-path `EMPTY` array).

## #914 (MethodDispatchIndex)
  - Differentiate the C3 and BFS test cases: both tests now use
    distinct MRO orderings so they prove the materializer stores
    whatever order the `computeMro` callback produces (not that C3 and
    BFS yield identical output).
  - Add `implementsOfCalls` counter in the first-write-wins test, and
    document the call-count contract in `MethodDispatchInput.implementsOf`
    JSDoc: `implementsOf` fires **per occurrence** in `input.owners`
    (not per unique owner); `computeMro` fires at most once per unique
    owner. Callers with expensive `implementsOf` implementations should
    pre-dedupe `owners`.

## #916 (resolveTypeRef)
  - Document the deliberate exclusion of `'Type'` from `TYPE_KINDS`
    (verified no extractor in `gitnexus/src/core/ingestion/` emits
    `type: 'Type'` for annotation-relevant symbols).
  - Rename the namespace-origin test from `'resolves ...'` to
    `'returns null for a namespace-origin binding whose def is not a
    type kind'`, matching the failure-case intent.

## #918 (shadow diff + aggregate)
  - Remove the partial re-export `export type { ShadowAgreement, ShadowDiff };`
    from `aggregate.ts` — it omitted `ShadowCallsite` and diverged
    from the top-level barrel. Consumers import all three from the
    `gitnexus-shared` entry point.
  - Fix the invalid `'wildcard'` evidence kind in `diff.test.ts` fixture
    (that kind is not a valid `ResolutionEvidence.kind`). Replaced with
    `'global-name'`, a real kind the test treats identically.

## #912 (ScopeTree / PositionIndex / makeScopeId)
  - Document the touching-boundary semantics on `PositionIndex.atPosition`:
    when siblings share a boundary point, the right (later-start) sibling
    wins per the existing innermost-wins sort contract.
  - Resolve the layer-inversion flagged by review: move `ScopeLookup`
    from `resolve-type-ref.ts` to `types.ts` (its natural home in the
    data-model layer). `scope-tree.ts` now imports `ScopeLookup` from
    `types.js` directly; the old re-export from `resolve-type-ref.ts`
    is removed per repo convention (`feedback_no_reexport`). Barrel
    export moved alongside.

## #917 (ClassRegistry / MethodRegistry / FieldRegistry)
  - Replace the dangling "try a name-match among class-like defs"
    comment in `lookupReceiverType` with explicit prose that callers
    must pre-resolve via `resolveTypeRef` if they want richer semantics.
    No behavior change — the function already returned `undefined` on
    ambiguous/missing qnames.
  - Fix `tieBreakKey.origin` default for pure Step-2 candidates.
    Type-binding-only hits no longer falsely inherit `'local'` from
    `ensureCandidate`'s neutral default; they now demote to `'import'`
    on their first type-binding hit, and only a later Step-1 lexical
    hit can upgrade them back to `'local'`. Keeps the Appendix B
    cascade faithful to the true origin.
  - Document `'global-name'` in `evidence.ts`: currently reserved for
    Ring 3's byName global index; `lookupCore` never emits it today.
    The weight stays live so `composeEvidence` remains exhaustive over
    the origin union.
  - Rename the mislabeled Step-7 test from `'confidence DESC is the
    primary key'` (which actually tested hard-shadow baseline) to
    `'inner scope shadows outer, yielding single result'`, and add a
    separate test that actually exercises multi-candidate confidence
    ordering (local vs wildcard at the same scope).

## Verification
  - `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`)
  - `gitnexus-shared` build clean
  - Combined scope-resolution / model / shadow suite: **260/260 pass**
    (+1 from the new multi-candidate ordering test in #917)

## Not addressed (non-actionable)
  - #949 CI "failure with zero failing tests": pre-existing Swift Node 22
    grammar flake unrelated to #910 scope.
  - #950: the two non-blocking findings were already addressed in
    follow-up commit `cbac32ba` (ParsedImport discriminated union +
    `ScopeId | null` on the two hooks).
  - #915: the five in-scope findings were already addressed in
    follow-up commit `54515a7e` (dead code, unused params, multi-hop
    docs, cap-hit test, stats granularity).
  - #915 LanguageProvider.resolveImportTarget signature divergence +
    `findDefById` O(F×D) perf: tracked separately as follow-up issues
    for the Ring 3 migration window.

* chore(shared): address ce:review findings on the follow-up diff

ce:review (interactive) on PR #964 surfaced two P2s and several P3s. This
commit applies all `safe_auto` fixes + both manual tests in-line so the
PR ships with a cleaner review trail.

## P2 fixes

- **Complete `freezeIndex` → `wrapIndex` rename.** The prior commit renamed
  3 of 5 sibling index files; `method-dispatch-index.ts` and
  `position-index.ts` still carried the old name. Now all 5 helpers use
  the consistent `wrapIndex` naming.
  (maintainability + project-standards reviewers both flagged this.)

- **Add regression tests for the `recordTypeBindingHit` origin demotion.**
  The prior commit introduced the `tieBreakKey.origin = 'import'`
  demotion for Step-2-only candidates without a direct test. Added:
    - `registries.test.ts`: two Step-2-only siblings under the same
      interface, asserting deterministic DefId.localeCompare tie-break
      AND the stronger invariant that composeEvidence never emits a
      where-found signal for Step-2-only candidates (no `signals.origin`).
    - `position-index.test.ts`: touching-boundary test proving the
      right-sibling-wins rule documented in the new JSDoc.
  (testing + kieran-typescript + api-contract reviewers all flagged these gaps.)

## P3 fixes

- Fix wrong comment in `recordTypeBindingHit` that claimed Step 1 could
  later upgrade a demoted origin. Step 1 runs BEFORE Step 2 — the actual
  upgrade path is Step 3 (`seedFromOwnerScopedContributor`). Comment now
  describes execution order correctly.

- Fix inaccurate "re-exported there" comment in `index.ts`. `types.ts`
  *defines* ScopeLookup natively; it's not a re-export. Phrasing now
  says "defined in types.ts and exported from the type-export block
  above — not from this module."

- Update stale `scope-tree.ts` file-header prose that still referenced
  `ScopeLookup` as living in #916/resolve-type-ref.ts. Now points to
  `./types.js` with a cross-ref to both #916 and #917 consumers.

- Expand `atPosition` touching-boundary JSDoc to name the mechanism
  (backward scan through start-sorted array) so readers can trace the
  binary-search code to the claim.

- Add breadcrumb to `aggregate.ts` module header pointing future readers
  to `./diff.ts` / the top-level barrel for `ShadowAgreement`,
  `ShadowCallsite`, and `ShadowDiff`.

- Remove unnecessary non-null assertion in `recordTypeBindingHit`. Local
  `const existingMroDepth = ...` lets TS narrow to `number` in the
  else-branch, eliminating the `!` without behavior change.

## Verification

- `tsc --noEmit` clean (both `gitnexus-shared` and `gitnexus`)
- `gitnexus-shared` build clean
- Combined scope-resolution / model / shadow suite: **262/262 pass** (+2
  from the new origin-demotion + touching-boundary regression tests)
2026-04-18 18:40:29 +01:00
Gergő Magyar
1bf9fb4ef1
feat(shared): ClassRegistry / MethodRegistry / FieldRegistry + 7-step lookup (#917, RFC #909 Ring 2 SHARED) (#963)
Capstone of Ring 2 SHARED. Implements RFC §4 — the shared, scope-aware
resolution surface the rest of the semantic model feeds into.

## Modules (`gitnexus-shared/src/scope-resolution/registries/`)

  - `context.ts`         — `RegistryContext` bundling ScopeTree / DefIndex
                           / QualifiedNameIndex / ModuleScopeIndex /
                           MethodDispatchIndex + provider hooks.
                           Narrows Ring 1's opaque `RegistryContributor`
                           to concrete `OwnerScopedContributor`.
  - `tie-breaks.ts`      — `compareByConfidenceWithTiebreaks`, the RFC
                           Appendix B cascade: confidence DESC → scope
                           depth ASC → MRO depth ASC → ORIGIN_PRIORITY
                           ASC → DefId.localeCompare.
  - `evidence.ts`        — `composeEvidence(signals)` / `confidenceFromEvidence`.
                           Translates raw walk signals into the typed
                           `ResolutionEvidence[]` using authoritative
                           `EvidenceWeights`. No magic numbers.
  - `lookup-qualified.ts`— RFC §4.5. Qualified-name fast path consumed
                           by `resolveTypeRef` dotted fallback and by
                           Step 6 of lookup-core.
  - `lookup-core.ts`     — The 7-step canonical algorithm. Pure. Param-
                           eterized by `CoreLookupParams`.
  - `{class,method,field}-registry.ts`
                         — Thin wrappers over `lookupCore` that fix
                           `acceptedKinds` + `useReceiverTypeBinding` per
                           kind. `buildClassRegistry` / `buildMethodRegistry`
                           / `buildFieldRegistry` factory functions.

## RFC §4.2 algorithm contract (honored verbatim)

  1. Lexical scope-chain walk. Hard shadow on any `scope.bindings.has(name)`
     regardless of kind survivorship.
  2. Type-binding resolution (methods/fields only, opt-in via
     `useReceiverTypeBinding`). MRO walk via `MethodDispatchIndex.mroFor`.
     MRO-depth-decayed weight via `typeBindingWeightAtDepth`.
  3. Owner-scoped contributor — when the caller knows the receiver owner,
     its direct members merge in as `origin: 'local'`.
  4. Kind filter — `acceptedKinds` per registry; `kind-match` evidence
     at weight 0 is always emitted for debuggability.
  5. Arity filter — `provider.arityCompatibility` per candidate. When at
     least one compatible candidate exists, incompatibles are dropped;
     otherwise the −0.15 penalty alone disambiguates (they stay in the
     result, just ranked lower).
  6. Global fallback — fires only when Steps 1-3 produced NO candidates
     AND the name is dotted. Delegates to `lookupQualified`.
  7. Rank + tie-break — evidence list sorted by the Appendix B cascade.

## §4.7 invariants asserted in tests

  - No tier vocabulary in the return type (`Resolution`, not `TierXResult`).
  - Confidence is per-candidate (not per-tier).
  - Shadowing is a HARD filter; globals are consulted ONLY when lexically
    empty.
  - Caller can read `[0]` for one-shot answers.
  - `Resolution.confidence` is capped at 1.0.
  - `kind-match` is always emitted (weight 0).

## Unresolved-import + dynamic-unresolved evidence shape

  - `BindingRef.via.linkStatus === 'unresolved'` applies the
    `unlinkedImportMultiplier` (0.5×) to the where-found signal only.
    Corroborators (`arity-match`, `owner-match`, `type-binding`) remain
    unaffected — the RFC §4v2 capped-signal rule applies per-signal, not
    per-candidate.
  - `BindingRef.via.kind === 'dynamic-unresolved'` adds a degraded
    `dynamic-import-unresolved` evidence signal at weight 0.02.

## Tests (28 in registries.test.ts, 259/259 combined)

Organized per RFC §4.2 step so a regression localizes to the step it broke:

  - Step 1: local + walk-to-parent + hard-shadow + origin=import
  - Step 2: explicit receiver type-binding + MRO depth decay on ancestor
  - Step 3: owner-scoped contributor + owner-match
  - Step 5: drop-incompatible-when-compatible-exists + soft-penalty-when-all-
            incompatible + unknown-when-no-provider
  - Step 6: global-qualified fires only when lexically empty + never for
            non-dotted names + not consulted when lexical hit exists
  - Step 7: tie-break cascade (inner shadows outer; defId.localeCompare
            final)
  - Corroborators: unresolved-import 0.5× cap per-signal + dynamic-
            unresolved 0.02 degraded signal
  - §4.5: lookupQualified kind filter + empty on miss + deterministic defId
          order for partial classes
  - §4.7: invariants — confidence per-candidate, capped at 1.0, kind-match
          always present, [0]-for-one-shot

## Known follow-up optimizations

`collectOwnedMembers` in `lookup-core.ts` iterates `defs.byId.values()`
for each MRO hop — O(D) per call. Acceptable for Ring 2 fixtures; a
by-owner index should land before Ring 3 migrates large-workspace
languages. Tracked alongside the existing `findDefById` follow-up from
#915 review.

## Module placement

All under `gitnexus-shared/src/scope-resolution/registries/` — consistent
with the Ring 2 SHARED folder layout (#912/#913/#914/#915/#916/#918).
Slight deviation from the issue's `gitnexus-shared/src/registries/`
suggestion for consistency with siblings.

## Part of

- Parent: #909
- Depends on (code): #910, #911, #912, #913, #914, #915, #916, #918.
- Closes the Ring 2 SHARED delivery band. Unblocks Ring 2 PKG (#919–#925
  bridges to the gitnexus/ CLI package) and Ring 3 language migrations.
2026-04-18 17:58:26 +01:00
Gergő Magyar
a9a5e1c388
feat(shared): SCC-aware finalize algorithm with bounded fixpoint (#915, RFC #909 Ring 2 SHARED) (#962)
* feat(shared): SCC-aware finalize algorithm with bounded fixpoint (#915, RFC #909 Ring 2 SHARED)

Implements RFC §3.2 Phase 2 as pure logic in `gitnexus-shared`. Takes
per-file parse output and returns linked `ImportEdge[]` + materialized
module-scope bindings, fully language-agnostic (target resolution,
wildcard expansion, and binding precedence all go through caller hooks).

Three-phase algorithm:

  1. Tarjan SCC over the file-level import graph (iterative, deterministic
     node order, O(V+E)). Returns SCCs in reverse-topological order so
     leaves finalize before dependents — and so disjoint SCCs are
     explicitly surfaced for parallel-processing callers.

  2. Per-SCC bounded fixpoint. For each SCC in topo order, iterate up to
     `N = |intra-SCC edges|`; each pass tries to resolve every still-
     unlinked edge by looking up the imported name in the target file's
     local defs. Stops early when no progress. Edges still unlinked after
     the cap get `linkStatus: 'unresolved'` — keeps malformed inputs
     bounded and preserves the RFC §4v2 capped-signal contract for
     unresolved markers.

  3. Wildcard expansion + module-scope binding materialization. For each
     `wildcard` ParsedImport that linked to a module, expand via
     `expandsWildcardTo` into one `wildcard-expanded` ImportEdge per
     exported name. Bindings per module scope are the merge of local defs
     (`origin: 'local'`), named / alias / reexport imports
     (`origin: 'import' | 'reexport'`), namespace imports (`origin:
     'namespace'`), and wildcard expansions (`origin: 'wildcard'`), with
     precedence delegated to `provider.mergeBindings`.

Dynamic imports rule: `kind: 'dynamic-unresolved'` passes through as an
ImportEdge with `targetFile: null` and no BindingRef.

Re-export flattening: reexport edges land with `transitiveVia: [targetFile]`.
Multi-hop chains settle iteratively across the fixpoint.

Types:
  - Adds `'wildcard'` variant to ParsedImport (parse-time signal for
    `import * from M`). The finalize-only `'wildcard-expanded'` ImportEdge
    kind is unchanged and remains finalize output only, as documented.
  - Exports `finalize` + `FinalizeFile` / `FinalizeInput` / `FinalizeHooks`
    / `FinalizeOutput` / `FinalizedScc` / `FinalizeStats`.

Simple-name derivation: `deriveSimpleName` uses `def.qualifiedName` as the
authoritative source (tail after the last `.`). Defs without a
qualifiedName are not name-resolvable by this algorithm — an explicit
design choice that trades strictness for predictability (no heuristic
nodeId parsing).

Tests (20, all passing):
  - Trivial: empty workspace · acyclic resolution · unresolvable target
    (file + name) · dynamic-unresolved passthrough.
  - Cycles: A↔B two-file cycle linked · cycles packed into SCC with
    isCycle=true · disjoint cycles produce disjoint SCCs · mixed
    linked/unresolved edges reported correctly in stats.
  - Wildcards: one ImportEdge per exported name · unresolved wildcards
    survive as single edges · expanded bindings carry origin='wildcard'.
  - Reexports: transitiveVia carries the intermediate file path.
  - Aliased + namespace: alias preserves targetExportedName under its
    local name · namespace links to module scope even without a module-def.
  - Bindings: locals land as origin='local' · imports layer on via
    mergeBindings · mergeBindings can drop existing (last-write-wins
    precedence honored).
  - SCC-DAG: reverse-topological ordering verified (leaf first).

Combined scope-resolution / model / shadow suite: 229/229 pass.
`tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus`.

Closes part of #909. Unblocks #917 (Registry.lookup's import-chain fast
path consumes finalized ImportEdges); unblocks Ring 3 language migrations
(per-language providers supply FinalizeHooks implementations).

* chore(shared): address #915 review findings — dead code, docs, tests

Review thread on PR #962.

Code changes:
  - Remove dead `resolvedTargets` map + `keyFor` + `ParsedImportKey`
    type alias. The map was populated but never read; originally intended
    to cache / dedup resolutions for later phases but that path was never
    wired (finding 1.1).
  - Drop unused params (`_edgeIndex`, `_hooks`, `_workspace`) from
    `tryFinalize`. No planned fixpoint-state consultation; no reason to
    keep them reserved (finding 2.1).

Documentation:
  - `FinalizeFile.localDefs` now documents the multi-hop re-export
    contract explicitly: `finalize` looks names up in the target's
    static `localDefs`; if B only re-exports from C and doesn't surface
    the name in its own localDefs, A's import of that name from B will
    hit the cap and be marked unresolved. Parsers that want multi-hop
    chains to settle end-to-end must include re-exported names in the
    intermediate file's localDefs (finding 1.2).
  - `FinalizeStats` now documents its counting granularity: all edge
    counters are per-`ParsedImport`, not per-materialized-`ImportEdge`.
    A wildcard expanding to N exports counts as one linked edge;
    dynamic-unresolved pass-throughs count as linked. The bindings map
    is the authoritative "has a BindingRef" source (finding 3.2).

Tests (2 added, 22 total in finalize-algorithm.test.ts, 231/231 combined):
  - Explicit cap-hit → `linkStatus: 'unresolved'` assertion for a cycle
    where the name-level lookup never succeeds (distinct from
    `targetFile: null`; cap exhaustion path) (finding 3.1).
  - Multi-hop re-export contract test: demonstrates both variants —
    intermediate B WITHOUT X in localDefs → unresolved; B WITH X in
    localDefs → resolved to the original source DefId (finding 1.2).

Not addressed (filed as follow-up issues):
  - LanguageProvider.resolveImportTarget vs FinalizeHooks signature
    divergence (finding 1.3) — pre-Ring-3 concern.
  - findDefById O(F×D) scan in Phase 5 (finding 4.1) — acceptable for
    Ring 2; optimize before large-workspace Ring 3 migrations.
2026-04-18 17:26:07 +01:00
Gergő Magyar
8cf9ae0e0d
feat(shared): ScopeTree + PositionIndex + makeScopeId (#912, RFC #909 Ring 2 SHARED) (#961)
Implements the scope-tree spine and position-indexed lookup as pure logic
in `gitnexus-shared`. Generalizes the `enclosingFunctions` pattern from
closed PR #902 to arbitrary `ScopeKind`s.

Three modules under `gitnexus-shared/src/scope-resolution/`:

1. `scope-id.ts` — `makeScopeId({filePath, range, kind})` builds the
   canonical RFC §2.2 shape
     `scope:{filePath}#{startLine}:{startCol}-{endLine}:{endCol}:{kind}`
   and interns the result through a process-local pool so repeated calls
   with structurally identical inputs return the same string reference.
   `clearScopeIdInternPool()` exported for test isolation.

2. `scope-tree.ts` — `buildScopeTree(scopes)` validates invariants and
   returns an immutable `ScopeTree`:
     - `getScope(id)` / `getParent(id)` / `getChildren(id)` / `getAncestors(id)`
     - Implements the `ScopeLookup` contract from #916, so `resolveTypeRef`
       can consume a `ScopeTree` directly (test included).
   Invariants enforced (throw `ScopeTreeInvariantError` on violation):
     - Non-Module scopes must have a parent.
     - Parent must exist in the supplied set.
     - Parent range STRICTLY contains child range (equal ranges rejected).
     - Sibling ranges under the same parent do not overlap. Ranges that
       merely touch at the boundary (`a.end == b.start`) are accepted.
     - Parent and child live in the same filePath.
     - Duplicate scope ids are rejected.

3. `position-index.ts` — `buildPositionIndex(scopes)` produces a
   `PositionIndex` with `atPosition(filePath, line, col)`. Per-file sorted
   array; binary-search the upper bound of `start ≤ query`, scan backward
   through the prefix, return the first containing hit.
   Complexity: `O(log N_file + D)` typical (D = lexical depth ≤ ~10);
   degrades to `O(N_file)` only under pathological inputs (many scopes
   starting at the same position). "Innermost wins" falls out of the sort
   + backward-scan contract because `ScopeTree`'s invariants guarantee
   that scopes containing a point form an ancestor chain.

Types:
  - `ScopeTree` now exported from `scope-tree.ts`. The Ring 1 opaque
    placeholder in `types.ts` has been removed; LanguageProvider hooks
    that previously took `ScopeTree = unknown` now receive the concrete
    interface (CLI `tsc --noEmit` passes — no existing callers rely on
    the opaque shape).

Tests (39, all passing):
  - scope-id: canonical shape · all six ScopeKinds encoded · identity
    equality (same inputs → same reference) · distinguished by
    filePath / range / kind · purity under repeated calls · intern-pool
    clear preserves canonical shape.
  - scope-tree: empty tree · single module · nested Module→Class→Function
    · multiple siblings input-order preserved · ScopeLookup integration
    with resolveTypeRef · frozen children and ancestor arrays · all six
    invariant violations (non-Module orphan, parent-not-found, parent
    doesn't contain, parent == child, siblings overlap, cross-file parent,
    duplicate id) · boundary-touching siblings accepted.
  - position-index: empty · unindexed filePath · before/after-file
    queries · start/end inclusivity · innermost-wins for nested / co-
    starting / co-ending / same-line scopes · sibling dispatch · multi-
    file isolation · size · id-dedup.

Combined scope-resolution / model / shadow suite: 190/190 pass.
`tsc --noEmit` clean in both `gitnexus-shared` and `gitnexus`.

Closes part of #909. Unblocks #917 (`Registry.lookup` needs the scope
spine); makes `ScopeLookup` in #916 concrete without API churn.
2026-04-18 16:41:38 +01:00
azizur100389
ac148612ab
feat(search): per-phase timing instrumentation for the query pipeline (#953)
* feat(search): per-phase timing instrumentation for the query pipeline

The eval harness already measures search-pipeline latency per phase,
but the *product* query() tool has no timing visibility. That leaves
production latency opaque:

 - Is BM25 the tail, or vector search?
 - How much Promise.all overlap do concurrent searches actually save?
 - Does symbol_lookup dominate when per-symbol Cypher round-trips pile up?

None of this is answerable from the outside, which blocks the
latency-quality Pareto work tracked in #546 / #553.

Changes:

* New PhaseTimer class at src/core/search/phase-timer.ts.
  Supports three APIs:
    - start(phase) / stop() for sequential phases (per issue spec)
    - mark(phase, durationMs) for pre-measured durations
    - time(phase, promise) to wrap a promise inside Promise.all

  The issue's original spec was sequential-only, which doesn't work
  for BM25 + vector inside Promise.all — the second start() would
  auto-stop the first and only one phase would get timed. The mark()
  and time() variants resolve that without changing the sequential
  API for the other phases.

* local-backend.ts query() instrumented across seven phase markers:
    bm25, vector   (concurrent via timer.time inside Promise.all)
    merge          (RRF reciprocal-rank-fusion)
    symbol_lookup  (per-symbol process + cohesion + content Cypher)
    ranking        (in-memory priority sort)
    formatting     (response object construction + dedup)
    wall           (end-to-end; separate mark so callers can compare
                   sum(phases) vs wall and see Promise.all savings)

* logQueryTiming() helper next to logQueryError(), same console-based
  pattern (repo has no structured logger). Emits
    GitNexus [query:timing] query="..." totalMs=N phases={...}
  to stdout — greppable prefix, JSON-parseable payload, no new deps.

* timing: Record<string, number> added as a top-level field on the
  query() response. Strict superset of the previous shape — existing
  tests only assert field presence, so no regression. Other MCP tools
  use the same top-level-metadata convention (status, row_count,
  warning) rather than a nested _meta wrapper.

Tests:

 - 6 new unit tests for PhaseTimer covering start/stop, implicit
   stop-on-start, additive mark(), Promise.all-safe time(),
   negative/NaN rejection, and totalMs auto-stop.
 - 3 new assertions on the existing query integration test verifying
   timing.wall is a non-negative number and at least one of
   bm25/vector fired.

Verification:
  npx vitest run test/unit/phase-timer.test.ts       -> 6 pass
  npx vitest run test/unit/calltool-dispatch.test.ts -> 65 pass
  npx vitest run test/integration/local-backend-calltool.test.ts -> 18 pass
  npm run test:unit                                   -> 3777 pass
    (4 pre-existing env failures unchanged: skip-git-cli needs
     built dist/, git-utils tmpdir on Windows worktree)
  npx tsc --noEmit                                    -> clean

Scope declined for v1:

 - In-process histogram aggregation — the log line is enough for
   external tooling
 - Pareto curve generation — issue asks to enable it, not generate it
 - Sub-phases of symbol_lookup (process vs cohesion vs content) —
   issue lists them under one bucket; can split later if demand surfaces

Closes #553

* fix(search): route query:timing log to stderr to preserve stdio MCP contract

CI (#953) failed the `query: JSON appears on stdout, not stderr`
e2e test in test/integration/cli-e2e.test.ts with:

  SyntaxError: Unexpected token 'G', "GitNexus [..." is not valid JSON

Root cause: my initial logQueryTiming() in 63fbdc4 used console.log,
which writes to stdout. The MCP stdio transport uses stdout
exclusively for JSON-RPC responses (#324), and the CLI e2e test
guards that contract by asserting stdout parses as JSON on every
tool invocation. The "GitNexus [query:timing] ..." line was
interleaving with the response JSON and breaking the parse.

Fix: route logQueryTiming through console.error instead. stderr is
the correct channel for human-readable diagnostics and it is what
the sibling logQueryError already uses for the same reason. The log
line format is otherwise unchanged -- still greppable, still
JSON-parseable payload.

Verification (local, with dist built):
  npx vitest run test/integration/cli-e2e.test.ts -t "query: JSON"
    -> now passes (was failing across ubuntu/windows/macos in CI)
  npx tsc --noEmit                                  -> clean
  Two unrelated pre-existing failures on non-git
  directory handling remain (same on upstream/main).

Closes the CI regression introduced in 63fbdc4.
2026-04-18 16:30:07 +01:00
Gergő Magyar
5d76dbcfa2
feat(shared): MethodDispatchIndex materialized view over HeritageMap (#914, RFC #909 Ring 2 SHARED) (#960)
Implements RFC §3.1 `MethodDispatchIndex`: a two-way materialized view
keyed by `DefId` for O(1) method-dispatch resolution:

  - `mroByOwnerDefId`       — owner class → full MRO ancestor chain
                              (excludes self, per-language strategy order)
  - `implsByInterfaceDefId` — interface/trait → classes that implement it

**Not an MRO implementation.** `buildMethodDispatchIndex` is a pure
aggregator that calls back into caller-provided `computeMro` and
`implementsOf` functions. The five existing strategies (Python C3, Ruby
kind-aware, Java/Kotlin linear, Rust qualified-syntax, COBOL none) stay
where they are today (`model/resolve.ts`, `languages/ruby.ts`); this index
does not reimplement them.

Why callbacks rather than a shared registry: the strategies depend on the
CLI's `HeritageMap` + `SemanticModel`. Migrating both to `gitnexus-shared`
is out of scope for #914; callbacks let the shared build stay pure.

Module placement: `gitnexus-shared/src/scope-resolution/method-dispatch-index.ts`
for consistency with the other RFC §3.1 indexes (#913 DefIndex /
ModuleScopeIndex / QualifiedNameIndex; #916 resolveTypeRef).

Safety surface mirrors sibling indexes:
  - First-write-wins on duplicate owners.
  - Repeated (interface, owner) pairs deduplicated.
  - Stored arrays are `Object.freeze`d; caller mutation of the source
    array does not leak into the index.
  - Miss returns a shared frozen empty array.

Tests (19, all passing): empty input, single-inheritance chain, Python
C3 diamond, Java BFS, Ruby kind-aware mixin, Rust qualified-syntax empty,
interface inversion (single, multiple, ordered), dedup within and across
callback calls, frozen miss + bucket arrays, callback-array isolation,
readonly Map iteration.

Closes part of #909.
2026-04-18 16:28:46 +01:00
Gergő Magyar
56e32b310b
feat(shared): resolveTypeRef strict single-return type resolver (#916, RFC #909 Ring 2 SHARED) (#959)
Implements RFC §4.6: a strict, pure resolver for `TypeRef`s used by
`Registry.lookup` Step 2 (type-binding propagation) and by any caller that
wants the single best type-target for an annotation without paying for the
full evidence pipeline.

Algorithm (strict):

  1. Walk the scope chain from `ref.declaredAtScope`:
     - Return the first binding for `rawName` whose origin is in
       `{'local','import','namespace','reexport'}` AND whose `def.type` is a
       type-kind (class-like, interface-like, enum-like, alias-like).
     - If bindings exist but none qualify (non-type shadow, wildcard-only
       origin), return null immediately — do NOT fall through to the global
       qualified-name index.
  2. If `rawName` is dotted and the scope walk produced no match, consult
     `QualifiedNameIndex.byQualifiedName`. Only accept a UNIQUE type-kind
     hit; ambiguous or non-type results return null.

`'wildcard'` is deliberately excluded from strict origins — a
wildcard-expanded name is too loose to anchor type resolution.

Module placement: `gitnexus-shared/src/scope-resolution/resolve-type-ref.ts`
(alongside sibling indexes) rather than the issue's suggested
`gitnexus-shared/src/resolve-type-ref.ts`, for consistency with the rest of
the RFC §2/§3 surface.

A minimal `ScopeLookup` interface is declared inline so #916 ships
standalone; #912's `ScopeTree` will satisfy this contract without change.

Closes part of #909.
2026-04-18 16:09:54 +01:00
Gergő Magyar
ac2012e5ed
feat(shared): DefIndex / ModuleScopeIndex / QualifiedNameIndex (#913, RFC #909 Ring 2 SHARED) (#958)
Three flat O(1) indexes + pure build functions over per-file artifacts.
Contract-only; no runtime behavior change yet — consumers (#917 Registry
lookups, #915 SCC finalize, #919 ScopeExtractor) wire in later.

Each index follows the same shape:
  - build function: flat input list → frozen immutable index
  - public interface: readonly Map + get/has/size accessors
  - first-write-wins on id/filePath collisions (upstream bug signal)
  - pure, side-effect-free, safe to call repeatedly

DefIndex — the global "what is this id?" lookup
  gitnexus-shared/src/scope-resolution/def-index.ts
  buildDefIndex(defs: readonly SymbolDefinition[]): DefIndex
    byId: ReadonlyMap<DefId, SymbolDefinition>
  Consumed by Registry.lookup (#917) to materialize DefId[] hits back to
  full SymbolDefinition records.

ModuleScopeIndex — `filePath → moduleScopeId` for cross-file hops
  gitnexus-shared/src/scope-resolution/module-scope-index.ts
  buildModuleScopeIndex(entries): ModuleScopeIndex
    byFilePath: ReadonlyMap<string, ScopeId>
  Consumed by the SCC finalize link pass (#915) to resolve
  ImportEdge.targetFile to a concrete module scope in constant time.

QualifiedNameIndex — cross-kind qualified-name fast path
  gitnexus-shared/src/scope-resolution/qualified-name-index.ts
  buildQualifiedNameIndex(defs: readonly SymbolDefinition[]): QualifiedNameIndex
    byQualifiedName: ReadonlyMap<string, readonly DefId[]>
  Returns DefId[] (not a single DefId) because partial classes, method
  overloads, and cross-kind collisions can legitimately share a
  qualifiedName. Callers filter by acceptedKinds at the lookup site.
  Consumed by Registry.lookup qualified fast path + resolveTypeRef
  dotted fallback (#916, #917).

Barrel re-exports added to gitnexus-shared/src/index.ts so consumers
import from 'gitnexus-shared' rather than deep paths.

Tests (gitnexus/test/unit/scope-resolution/, 23 total):
  def-index.test.ts (6):
    empty, single def, multiple distinct, first-write-wins collision,
    missing id returns undefined, byId direct iteration
  module-scope-index.test.ts (6):
    empty, single entry, multiple files, first-write-wins on duplicate
    filePath, missing returns undefined, byFilePath direct iteration
  qualified-name-index.test.ts (11):
    empty, single qnamed def, partial classes accumulate, input-order
    preservation, qname separation, skip undefined/empty qname, pair
    dedup, cross-kind indexing, frozen-empty-array on miss, direct
    iteration

Verification:
  - gitnexus-shared + gitnexus build clean (tsc + scripts/build.js)
  - test/unit/scope-resolution: 23/23 pass
  - model + shadow + scope-resolution combined: 129/129 pass
  - No runtime consumer wiring yet — indexes are standalone library
    functions that #915, #917, #919 will import when ready

Depends on #910 (SymbolDefinition, DefId, ScopeId types — already on main).
Unblocks #915 (finalize algorithm), #917 (Registry.lookup), #919
(ScopeExtractor materialization).
2026-04-18 15:59:34 +01:00
Copilot
f73389eac3
fix: ENOBUFS in detect_changes by setting maxBuffer on git/rg execFileSync (#957)
* Initial plan

* Fix ENOBUFS in detect_changes by setting maxBuffer on git/rg execFileSync

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/bb241ed0-3b39-431f-a242-b0c7ced9707b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-18 15:58:31 +01:00
Gergő Magyar
22f0beb057
feat(shared): shadow-mode diff + aggregate — full implementation (#918, RFC #909 Ring 2 SHARED) (#951)
Replaces the scaffold stubs with working pure-logic implementations plus
unit-test coverage for both functions. Unblocks Ring 2 PKG #923 (shadow
harness) to consume a concrete library instead of throwing scaffolds.

gitnexus-shared/src/scope-resolution/shadow/diff.ts
  `diffResolutions(callsite, legacy, newResult): ShadowDiff`
    - [0] on each side is the top match
    - both empty         → 'both-empty',   delta []
    - legacy empty only  → 'only-new',     delta = new top evidence
    - new empty only     → 'only-legacy',  delta = legacy top evidence
    - same top nodeId    → 'both-agree',   delta []
    - different nodeIds  → 'both-disagree',
                           delta = symmetric difference of evidence kinds
                           (legacy-only first in input order, then new-only)
  Evidence identity is `ResolutionEvidence.kind` — weight/note differences
  for the same kind do NOT produce delta entries. Rationale: the aggregator
  wants to know which *signals* explain a disagreement, not fluctuations
  in calibration values.

gitnexus-shared/src/scope-resolution/shadow/aggregate.ts
  `aggregateDiffs(diffs, now?): ShadowParityReport`
    - buckets by `SupportedLanguages`
    - tallies agreements, evidence-breakdown (divergences only — agree and
      empty rows do not contribute)
    - parity = bothAgree / (totalCalls - bothEmpty), yields 0 (not NaN)
      when the denominator is 0
    - perLanguage sorted alphabetically by enum value for stable output
    - evidenceBreakdown internally sorted by kind for stable output
    - overall = column-wise sum across languages
    - `now` parameter makes generatedAt deterministic in tests

gitnexus-shared/src/index.ts
  Re-exports the full shadow API: diffResolutions, aggregateDiffs, and all
  their types (ShadowAgreement, ShadowCallsite, ShadowDiff,
  LanguageParityRow, ShadowParityReport).

gitnexus/test/unit/shadow/diff.test.ts (13 tests)
  - 5 agreement outcomes
  - symmetric-by-kind evidence delta (disjoint, overlapping, fully-overlapping)
  - weight-only differences produce no delta
  - top-match only (ignores indices beyond [0])
  - callsite passthrough
  - delta ordering (legacy-only first, input order preserved)

gitnexus/test/unit/shadow/aggregate.test.ts (9 tests)
  - empty input
  - single language, all agree / mixed / all empty
  - multi-language bucketing + overall sum
  - alphabetical language sort
  - evidence breakdown scope
  - determinism via injected `now` + JSON round-trip identity

Verification:
  - gitnexus-shared + gitnexus build clean (tsc + scripts/build.js)
  - test/unit/shadow: 22/22 pass
  - test/unit/model + test/unit/shadow combined: 106/106 pass
  - No runtime behavior changes (shadow is invoked by #923, not yet wired)

Stacked on main (af1d278a). Depends on types from #910 (merged).
Unblocks: #923 (Ring 2 PKG — shadow harness wiring) — concrete library
to consume instead of scaffold stubs.

Plan: docs/plans/2026-04-18-001-refactor-911-senior-hooks-redesign-plan.md
is about #911; #918's scope is the scaffold+fill-in described in the PR
description of #951.
2026-04-18 15:32:51 +01:00
Gergő Magyar
af1d278a7e
feat(shared,ingestion): extend LanguageProvider with scope-resolution hooks (#911, RFC #909 Ring 1) (#950)
Adds the 14 optional scope-resolution hooks from RFC #909 §5.2 to
`LanguageProviderConfig` plus the supporting input/output types in
`gitnexus-shared`. Contract-only; no runtime behavior changes.

Review-driven refinements (addresses two non-blocking review comments on #950):

1. `ParsedImport` is now a 5-variant discriminated union, not a flat
   record. Each variant carries only its legal fields so invalid shapes
   are compile errors:
     - 'named', 'alias', 'namespace', 'reexport', 'dynamic-unresolved'
   'wildcard-expanded' is deliberately excluded — finalize materializes
   that kind; a provider must never emit it at parse time.
   'reexport' is a first-class parse-phase variant so syntactically-
   detectable re-exports (TS `export { X } from './y'`, Rust
   `pub use foo::bar`) keep their parse-time signal through to finalize
   rather than being re-derived by the SCC pass.
   `namespace` gains an `importedName` field so `import numpy as np`
   can carry both `localName: 'np'` and `importedName: 'numpy'`.
   `dynamic-unresolved.targetRaw` is `string | null` (was mandatory
   null) so providers can emit the unresolvable expression text for
   diagnostics when available.

2. `bindingScopeFor` and `importOwningScope` return type changed from
   `ScopeId` to `ScopeId | null`, aligning with the X | null convention
   used by the 12 sibling optional hooks (receiverBinding,
   resolveScopeKind, interpretTypeBinding, …). `null` = delegate to the
   central default. Enables partial overrides — a JS provider can
   return a hoisted scope for `var` and `null` for `let`/`const`
   without re-implementing the default lookup.
   Both hooks also gain a purity JSDoc contract: same inputs yield the
   same ScopeId (or null) across invocations; no closure over mutable
   state. Required to keep scope-tree construction deterministic.

   A richer callable-defaults pattern (typed BindingScopeDefaults /
   ImportOwningDefaults helper interfaces on a `defaults` parameter)
   was considered and deferred to Ring 2 PKG #919, where the concrete
   ScopeExtractor will exist to inform the helper shape. Designing that
   pattern before the first consumer would set cross-hook precedent
   based on a single motivating example.

Supporting types added to gitnexus-shared/src/scope-resolution/types.ts:
  - CaptureMatch, ParsedImport, ParsedTypeBinding
  - WorkspaceIndex, ScopeTree (opaque placeholders until Ring 2)
  - Callsite

14 hooks added to LanguageProviderConfig (all optional):
  Parse phase: emitScopeCaptures, interpretImport, receiverBinding,
    interpretTypeBinding, resolveScopeKind, shouldCreateScope,
    bindingScopeFor
  Finalize phase: resolveImportTarget, expandsWildcardTo,
    importOwningScope, mergeBindings
  Reference-extraction phase: classifyCallForm
  Resolution phase: shouldShadow, arityCompatibility

Verification:
  - gitnexus-shared builds clean (tsc)
  - gitnexus builds clean (scripts/build.js)
  - test/unit/model: 84/84 pass — no regressions
  - No provider needs updating (all hooks optional)
  - No BindingScopeDefaults/ImportOwningDefaults/defaults parameter
    introduced (deferred to #919)

Stacked on #910 (merged as afc0a8b6); rebased on main.
Tracking: #909 (meta). Unblocks Ring 2 PKG (#919 ScopeExtractor,
#922 import adapters) and all Ring 3 per-language migrations.

Plan: docs/plans/2026-04-18-001-refactor-911-senior-hooks-redesign-plan.md
2026-04-18 14:54:51 +01:00
Gergő Magyar
afc0a8b6c5
feat(shared): add scope-resolution types + constants (#910, RFC #909 Ring 1) (#949)
Lands the authoritative data model and constants for the pure scope-based
resolution RFC (#909) as Ring 1, part 1. No runtime behavior changes —
types + constants only.

New in gitnexus-shared/src/scope-resolution/:
  - types.ts — Scope, ScopeKind, ScopeId, DefId, Range, Capture,
    BindingRef, ImportEdge, TypeRef, Resolution, ResolutionEvidence,
    Reference, ReferenceIndex, LookupParams, RegistryContributor
  - evidence-weights.ts — EvidenceWeights constant map + typeBindingWeightAtDepth
    (RFC Appendix A)
  - origin-priority.ts — ORIGIN_PRIORITY constant map for deterministic
    tie-breaks (RFC Appendix B)
  - language-classification.ts — LanguageClassification type +
    LanguageClassifications map (production × 14, experimental × 2
    for vue/cobol; governs Ring 4 DAG-retirement gate)
  - symbol-definition.ts — SymbolDefinition moved from
    gitnexus/src/core/ingestion/model/symbol-table.ts so scope-resolution
    types can reference it from the shared package

Consumer updates:
  - symbol-table.ts: removes local SymbolDefinition declaration; imports
    from gitnexus-shared
  - model/index.ts: drops SymbolDefinition from barrel re-export per
    "direct imports from gitnexus-shared" convention (see
    gitnexus-shared feedback in project memory)
  - 9 source files + 5 test files: import SymbolDefinition directly
    from 'gitnexus-shared'

Verification:
  - gitnexus-shared builds clean (tsc)
  - gitnexus builds clean (scripts/build.js)
  - 131/132 unit test files pass; 3767 tests green
  - Zero behavior changes; SymbolDefinition shape unchanged

Blocks: #911 (LanguageProvider hook interface extensions) and all of
Ring 2 (#912-#925). Closes part of #909.
2026-04-18 12:55:09 +01:00
Gergő Magyar
d9da7d6692
fix(test): isolate cli-e2e from shared mini-repo fixture (#954)
Deterministic fix for the Windows-flaky pipeline-graph-golden test.

Root cause
  cli-e2e.test.ts wrote into the SHARED fixture directory
  (test/fixtures/mini-repo/) — git init, analyze run that creates
  AGENTS.md, CLAUDE.md, .claude/, .gitnexus/. When pipeline-graph-golden
  ran in parallel, its `cpSync` of the source directory could capture
  the mid-flight pollution before cli-e2e's afterAll cleanup fired.
  macOS/Ubuntu won the race often enough that the flake presented as
  Windows-only.

Fix
  cli-e2e now copies mini-repo into a fresh `mkdtemp`'d parent whose
  basename is `mini-repo` (preserving `--repo mini-repo` CLI lookup by
  basename), runs git-init there, and rm's the whole tmpdir in afterAll.
  The shared fixture source is never touched.

  Fallout from the cwd change: bare `--import tsx` specifiers (2
  spawnSync + 1 spawn) can't resolve `tsx` from an os.tmpdir cwd where
  there is no node_modules. Switched them to the already-existing
  `tsxImportUrl` (absolute file:// URL to the tsx loader), matching
  the `runCliOutsideProject` pattern that was already set up for this
  exact case.

  Updated the "MINI_REPO is inside the project tree" comment in the
  `status on non-indexed repo` test — MINI_REPO is now in os.tmpdir,
  so the rationale for using a separate throwaway tmp git repo is
  different (but still valid: previous tests in the suite create
  MINI_REPO/.gitnexus, which findRepo() would pick up).

  Also updated pipeline-graph-golden's comment explaining WHY it
  copies to tmp — it's now defense-in-depth rather than a necessity,
  so a future test that adds files to the source can't silently
  regress the golden.

Verification
  - 5x consecutive `cli-e2e + pipeline-graph-golden` runs: 20/20 pass
    (deterministic)
  - 3x full suite including pipeline.test: 27/27 pass
  - test/fixtures/mini-repo/ post-run contents: only `src/` —
    zero pollution from any test
  - macOS/Ubuntu behavior unchanged (they were passing; tmpdir
    isolation is purely additive)
2026-04-18 12:54:59 +01:00
azizur100389
131d411ae4
feat(mcp): rank context/impact disambiguation candidates and expose kind/file_path hints (#888)
* feat(mcp): rank context/impact disambiguation candidates and expose kind/file_path hints

The `context` MCP tool already returned `{ status: 'ambiguous', candidates }`
when a name hit multiple symbols, but the candidates were returned in
arbitrary DB order and the only hint it accepted was file_path. The
`impact` tool was worse: when its name resolver found multiple viable
matches it silently picked the first one from a priority UNION, with no
signal back to the caller that a different symbol might have been
intended.

Both failure modes were flagged in issue #470 and reconfirmed in the
comments by a second user who described impact as returning "incorrect
parsing results and meaningless tool calls" in the multi-match case.

Changes:

* Add `resolveSymbolCandidates(repo, query, hints)` private helper on
  LocalBackend. Single place that:
   - Short-circuits on direct uid (zero-ambiguity)
   - Runs the same name-or-qualified-id match as before, with LIMIT 20
     (was 10) so the ranker has headroom instead of arbitrary truncation
   - Preserves the #480 Class/Constructor preference -- when the only
     ambiguity is a Class and its own Constructor, the Class wins
     silently
   - Scores each candidate (pure TS, no extra DB round-trip): base 0.50,
     +0.40 for file_path match, +0.20 for kind match, plus a small
     kind-priority tiebreaker (Class > Interface > Function > Method >
     Constructor) when no explicit kind hint is given
   - Sorts desc by score with stable tiebreakers (shorter filePath,
     then lex uid)
   - Promotes to a single confident resolve when the top score is
     >= 0.95 AND beats the runner-up by >= 0.10 -- lets a strong hint
     cut through without forcing the caller through a disambiguation
     round-trip

* Rewire `context()` to use the shared helper. Response shape is a
  strict superset of today's: candidates gain a `score` field, the
  existing `{ uid, name, kind, filePath, line }` keys are preserved so
  every downstream consumer (rename, eval-server formatter, etc.) keeps
  working. New `kind` input hint accepted.

* Rewire `impact()` to use the shared helper. Now emits the same
  `{ status: 'ambiguous', candidates, impactedCount: 0, risk: 'UNKNOWN' }`
  shape instead of silent first-pick. New inputs accepted:
  `target_uid`, `file_path`, `kind`.

* Update tool schemas in mcp/tools.ts to advertise the new inputs and
  describe ranked disambiguation.

Backward compatibility:

The #480 Class/Constructor collapse is preserved and covered by the
existing java-class-impact integration test (still green). The
ambiguous response shape is a strict superset -- `eval-formatters`
unit test that parses the old shape is unchanged and still passes.
`impact` going from silent-first-pick to structured ambiguous is a
semantic improvement that is the entire point of the issue; callers
relying on silent first-pick now get an actionable response.

Scope declined for v1:

module/community hint -- the issue lists it as one of several hints,
but kind + file_path cover the vast majority of disambiguation needs
in practice, and a community-label filter requires an extra graph
query per candidate. Natural v2 follow-up.

Tests: calltool-dispatch.test.ts gains 5 new cases covering file_path
boost, kind hint boost, impact ambiguous shape, impact target_uid
short-circuit, and score field presence on the existing ambiguous
test. Plus the extended assertions on the existing
`context tool returns disambiguation for multiple matches`.

Verification:
  npx vitest run test/unit/calltool-dispatch.test.ts       -> 64 pass
  npx vitest run test/integration/java-class-impact.test.ts -> pass
  npm run test:unit                                         -> 3642 pass
    (4 pre-existing env failures unchanged: skip-git-cli needs built
    dist/, git-utils tmpdir on Windows worktree -- same on main)
  npx tsc --noEmit                                          -> clean

Closes #470

* fix(mcp): enrich labels from UNION when labels(n)[0] is empty; address review findings

CI on PR #888 caught 13 integration-test failures I did not cover locally:
my resolver refactor collected candidates via `labels(n)[0] AS type`, but
LadybugDB returns an empty string for that projection on certain node
types (most importantly Class). With an empty `type`, impact's downstream
`_runImpactBFS` no longer recognised `symType === 'Class' | 'Interface'`
and stopped seeding Constructor + File nodes into the frontier, so the
"impact(upstream) surfaces the file importer" assertion broke across 11
language fixtures plus 2 OVERRIDES filter tests.

The original impact resolver worked around this by running a prioritised
UNION across Class/Interface/Function/Method/Constructor and picking the
first hit. My refactor dropped that. Fix: keep the simple candidate MATCH
but enrich types afterward via a single scoped UNION query, so every
candidate carries an accurate label for both scoring and downstream
BFS seeding. The UID direct-lookup path is patched the same way.

Also addresses the findings from the senior reviewer on PR #888:

* MIGRATION.md: document the `impact` behavioural change (silent first-
  pick → structured `{ status: 'ambiguous', candidates }`) so downstream
  callers know to branch on `result.status` before reading byDepth/
  summary. `context` is unchanged shape-wise (strict superset).

* New test: `context tool promotes top candidate via scoring when
  multiple rows survive DB pre-filter`. The review flagged that the
  existing file_path test works only because the mock ignores WHERE
  parameters -- the scored-promotion path (top ≥ 0.95 AND gap > 0.09)
  wasn't directly exercised. The new test uses two candidates both in
  App.tsx-containing paths plus a kind hint so promotion is decided by
  scoring, not DB pre-filtering. Also tightened the comment on the
  earlier file_path test to describe the mock vs production divergence
  honestly.

* NIT: added a paragraph explaining why `scored.length >= 2` is kept as
  a defensive guard even though the `normalized.length === 1` early
  return already covers the single-candidate path.

* Integration: two tests in `local-backend-calltool.test.ts` targeted
  `'authenticate'`, which now correctly resolves as ambiguous (two
  Method nodes: AuthService.authenticate and BaseService.authenticate).
  Updated both to pass `file_path: 'src/auth.ts'` so they exercise the
  new disambiguation API and still assert the METHOD_OVERRIDES filtering
  they were originally about.

Edge case fix in the promotion gap check: IEEE754 makes 0.50 + 0.40 +
0.20 - 0.90 = 0.09999999999999998 instead of exactly 0.10, which would
otherwise break the "winner clearly dominates" intent for legitimate
1.00 vs 0.90 cases. Changed `>= 0.10` to `> 0.09`; same user-facing
intent, no floating-point sensitivity.

Verification (all from gitnexus/):
  npx vitest run test/integration/class-impact-all-languages.test.ts
    -> 52 pass (was 11 FAIL on CI before this fix)
  npx vitest run test/integration/local-backend-calltool.test.ts
    -> 18 pass (was 2 FAIL on CI before this fix)
  npx vitest run test/integration/java-class-impact.test.ts
    -> 10 pass (regression guard for #480 preserved)
  npx vitest run test/unit/calltool-dispatch.test.ts
    -> 65 pass (1 new test + 4 from original #470 PR)
  npm run test:unit
    -> 3626 pass, 4 pre-existing env failures unchanged
  npx tsc --noEmit
    -> clean
2026-04-18 12:52:42 +01:00
Gergő Magyar
b8875b9c80
chore(release): v1.6.2 (#952)
Bumps gitnexus to v1.6.2 and adds the matching CHANGELOG entry.

Highlights since v1.6.1 (61 commits):
  - Docker support (#848)
  - Language-agnostic heritage / call / variable extractors
    (config+factory pattern, #877 #878 #890)
  - AST-aware embedding chunking (#889)
  - jQuery / axios HTTP consumer detection (#887)
  - SemanticModel wired as first-class resolution input, SM-20 (#885)
  - ImportSemantics split into per-strategy hooks (#886)
  - Python dotted-import fix (#899); worker warnings non-terminal
    (#900 / #261); global-install ENOTEMPTY fixes (#843 #846);
    embeddings staleness fix (#831)

See gitnexus/CHANGELOG.md for the full list.

After merge, tag `v1.6.2` triggers publish.yml which runs CI,
verifies tag↔package.json match, publishes to npm with provenance,
and creates the GitHub Release using the extracted CHANGELOG body.
2026-04-18 12:16:51 +01:00
Ryanba
969b4623ca
fix: keep worker warnings non-terminal (#900)
* fix: keep worker warnings non-terminal

Treat parse-worker warning messages as informational so a warning can be surfaced without short-circuiting the worker result protocol.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* style: apply prettier formatting

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-18 11:09:15 +01:00
Copilot
018e0e6b14
test(web-e2e): raise status-ready timeout to 45s for parallel-worker stability (#908)
* Initial plan

* plan: stabilize web e2e tests timing out under parallel workers

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/49d09e67-5a8e-4eee-adcd-3d5416675a6b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test(web-e2e): bump status-ready timeout to 45s for parallel-worker stability

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/49d09e67-5a8e-4eee-adcd-3d5416675a6b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
2026-04-18 10:15:28 +01:00
Kritik Bangera
040bb7a489
feat: add docker support (#848)
* feat: add docker support

* feat: move docker files to root

* feat: add docker build and push workflow

* fix: pin docker action SHAs to verified commits

Made-with: Cursor

* fix: remove redundant --platform=$TARGETPLATFORM from runtime stage

Made-with: Cursor

* fix: upgrade docker actions to Node.js 24-compatible versions

Made-with: Cursor

* docs: updated readme

* fix: update docker references

* fix(docker-server): reject null bytes in resolvePath

Defensively harden the path traversal guard by returning null
early when the URL contains a null byte, before normalization runs.

Made-with: Cursor

* fix(docker-server): handle createReadStream errors

Attach an error listener before piping so mid-flight read errors
(truncated file, permission change) cleanly destroy the response
instead of being silently swallowed.

Made-with: Cursor

* fix(docker-server): replace existsSync with async stat

Eliminates the TOCTOU race between the initial stat call and the
subsequent existsSync check. Reuses the async stat pattern already
in place and removes the now-unused existsSync import.

Made-with: Cursor

* test(docker-server): add integration tests; fix %00 null-byte bypass

Decode the URL before the null-byte check so percent-encoded null
bytes (%00) are also rejected with 400 instead of falling through
to the SPA fallback. Adds 5 node:test integration tests covering
valid assets, SPA fallback, path traversal, null bytes, and 404.

Made-with: Cursor

* style: fix prettier formatting in docker-server files

Made-with: Cursor

* fix(docker): wire tests into CI, fix resolvePath separator, correct image namespace

- Add `node --test docker-server.test.mjs` step to ci-tests.yml so the
  path-traversal guard tests run in every CI pass instead of being silently skipped.
- Fix resolvePath containment check: `startsWith(root)` would allow sibling
  directories like `/app/dist-evil/`; now guards with `root + sep` or exact match.
- Update docker-compose.yaml default image from `abhigyanpatwari` namespace to
  `brainifii` to match what docker.yml publishes to GHCR.

* fix(docker): update apt-get commands and set user permissions

- Modify Dockerfile and Dockerfile.test to include options for apt-get to bypass validity checks during updates.
- Set ownership of the /app directory to the 'node' user in the runtime stage for improved security and proper permission handling.

* fix(docker): switch to Alpine base images for smaller footprint

- Update Dockerfile to use Alpine-based Node.js images for both builder and runtime stages, reducing image size and improving performance.
- Replace apt-get commands with apk for package installation in the runtime stage.

* fix(docker): update Node.js version in Dockerfile

- Change base image from node:20-alpine to node:22-alpine

* fix(docker): update Node.js version in Dockerfile to 22-alpine for runtime

---------

Co-authored-by: kritik.b <kritik.b@media.net>
2026-04-18 08:39:18 +01:00
dependabot[bot]
725ed3fe66
chore(deps)(deps): bump glob from 11.1.0 to 13.0.6 in /gitnexus (#867) 2026-04-18 07:40:08 +01:00
dependabot[bot]
509185b8f9
chore(deps)(deps): bump commander from 12.1.0 to 14.0.3 in /gitnexus (#868) 2026-04-18 07:28:17 +01:00
dependabot[bot]
4988feec94
chore(deps)(deps-dev): bump wait-on from 8.0.5 to 9.0.5 in /gitnexus-web (#859) 2026-04-18 07:26:56 +01:00
dependabot[bot]
ef953beca9
chore(deps)(deps): bump @huggingface/transformers in /gitnexus (#869) 2026-04-18 07:22:34 +01:00
dependabot[bot]
0b5381695f
chore(deps)(deps-dev): bump @vitest/coverage-v8 in /gitnexus (#864) 2026-04-18 07:21:02 +01:00
dependabot[bot]
30292d7179
chore(deps)(deps): bump @modelcontextprotocol/sdk in /gitnexus (#866) 2026-04-18 07:20:19 +01:00
dependabot[bot]
7a98a01ad5
chore(deps)(deps): bump lru-cache from 11.2.7 to 11.3.5 in /gitnexus (#870) 2026-04-18 07:19:49 +01:00
dependabot[bot]
94cba48b6d
chore(deps)(deps): bump mnemonist from 0.39.8 to 0.40.3 in /gitnexus (#871) 2026-04-18 07:19:24 +01:00
azizur100389
925460ab5b
refactor(cli): trim duplicated ai-context CLAUDE.md block (#904) 2026-04-18 07:10:44 +01:00
dependabot[bot]
08b4505197
chore(deps)(deps): bump @ladybugdb/core in /gitnexus (#873)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
2026-04-17 21:28:34 +01:00
dependabot[bot]
d5225e699f
chore(deps)(deps): bump mermaid from 11.12.2 to 11.14.0 in /gitnexus-web (#860) 2026-04-17 21:27:37 +01:00
dependabot[bot]
0a3b9120a0
chore(deps)(deps): bump tailwindcss in /gitnexus-web (#861) 2026-04-17 21:27:07 +01:00
dependabot[bot]
5544350e30
chore(deps)(deps-dev): bump jsdom from 29.0.0 to 29.0.2 in /gitnexus-web (#863) 2026-04-17 21:26:50 +01:00
Copilot
dfa449ef41
feat(ingestion): language-agnostic heritage extractor with config+factory pattern (#890) 2026-04-17 17:51:17 +01:00
Yacine Hmito
daca8360bf
fix(python): avoid local matches for external dotted imports (#899) 2026-04-17 11:35:59 +01:00
Ryanba
77a13113ea
fix: keep worker warnings non-terminal (#261) 2026-04-17 06:46:31 +01:00
evolution
02739085d2
feat(embeddings): AST-aware chunking with offset-based splitting (#889)
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
2026-04-16 22:55:04 +01:00
Copilot
43098784cf
refactor(ingestion): split ImportSemantics into per-strategy hooks (Strategies 1-4) (#886)
* Initial plan

* refactor(ingestion): split ImportSemantics into per-strategy hooks

- Add ImportResolverStrategy and ImportResolutionConfig types
- Create createImportResolver factory (resolver-factory.ts)
- Add createStandardStrategy to standard.ts
- Extract per-language strategies from existing resolvers:
  goPackageStrategy, javaJvmStrategy, kotlinJvmStrategy,
  rustModuleStrategy, pythonImportStrategy, csharpNamespaceStrategy,
  phpPsr4Strategy, swiftPackageStrategy, dartPackageStrategy,
  dartRelativeStrategy, rubyRequireStrategy
- Create per-language config files in import-resolvers/configs/
- Update all 15 language providers to use createImportResolver(config)
- Add 38 unit tests for factory and strategy composition
- All 3640+ existing tests pass, tsc --noEmit passes

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c3aec32d-2155-4808-88df-9cd6b2384174

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* chore: remove unused resolver imports from language providers

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c3aec32d-2155-4808-88df-9cd6b2384174

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* docs: add error propagation note to createImportResolver JSDoc

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/c3aec32d-2155-4808-88df-9cd6b2384174

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor: consolidate strategies into configs, remove legacy resolvers

- Move all strategies from per-language files into their config files
- Remove swift.ts and vue.ts (no shared helpers needed)
- Remove legacy monolithic resolver functions from all per-language files
- Remove unused legacy wrapper functions from standard.ts
- Per-language files now only contain shared internal helpers
- Fix lint warning in languages/php.ts (no-non-null-assertion)
- Update test imports to reference configs/ instead of per-language files
- All 3262+ tests pass, tsc --noEmit passes, zero lint errors

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/f8da6bc2-957c-4d20-87ba-402fa223c6c8

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor: address review feedback — remove dart.ts shim, JSDoc language field, update ARCHITECTURE.md

- Add JSDoc to ImportResolutionConfig.language clarifying it's
  documentation-only metadata not used by the factory
- Remove dart.ts legacy shim (was only kept for backward-compat tests)
- Rewrite dart-import-resolver.test.ts to test production strategies
  (dartPackageStrategy/dartRelativeStrategy) directly, including full
  factory composition via dartImportConfig
- Fix lint warning (no-explicit-any) by using buildSuffixIndex in makeCtx
- Update ARCHITECTURE.md to mention import-resolvers/configs/ as the
  extension point for per-language import resolution

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/53f09a4f-1ff1-4a3e-a29c-fda9cdb4c4ef

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix: address review comments — revert php.ts, tighten dart test assertion

- Revert php.ts: restore stack.pop()! (the while guard guarantees non-empty)
- Tighten dart relative import test to assert exact result instead of
  permissive null-or-files check

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/53f09a4f-1ff1-4a3e-a29c-fda9cdb4c4ef

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* refactor(ingestion): strengthen import-resolver tests and document Vue config intent

Address non-blocking follow-ups from PR #886 review:
- Add inline comment to vueImportConfig explaining intentional
  language: Vue / TypeScript-strategy mismatch (Vue SFCs are
  preprocessed into TS upstream of import resolution).
- Replace 11 tautological typeof === 'function' assertions with
  behavioral tests for goPackageStrategy, kotlinJvmStrategy, and
  csharpNamespaceStrategy, including full-chain strategy-order
  guards via createImportResolver(config).
- Apply prettier formatting to sibling configs touched during
  factory introduction.

Test: 37 passed (previously 26), tsc --noEmit clean.

* test(ingestion): tighten import-resolver assertions and close coverage gaps

Apply ce-review findings on commit f4be87fb:

- Tighten dirSuffix assertions from toContain() to exact toEqual()
  shape, catching format regressions (slash normalization, prefix
  trimming) the loose matcher would miss.
- Collapse 'if (result?.kind === "package") { expect(dirSuffix)... }'
  conditional-dead-branch pattern into single toEqual() assertions.
- Add goPackageStrategy fall-through test: module prefix matches but
  package directory contains no .go files -> null (documented branch
  in configs/go.ts:27 had no coverage).
- Honestly relabel kotlinImportConfig full-chain test as a behavioral
  smoke test rather than a strategy-order guard — standard.ts:137
  returns null for '.*' imports so reordering is not observable via
  wildcard inputs. Added Kotlin member-import test for extra coverage.
- Add behavioral tests for javaJvmStrategy, rustModuleStrategy,
  phpPsr4Strategy, swiftPackageStrategy, rubyRequireStrategy (10 tests
  across 5 describe blocks) so strategy unwiring would be caught.
- Extend makeCtx() with optional overrides: Partial<ResolveCtx['configs']>
  parameter for declarative per-test config setup.

Test: 50 passed (previously 37), tsc --noEmit clean.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-16 19:31:44 +01:00