Commit graph

741 commits

Author SHA1 Message Date
Sparsh
13eeab4601 feat(desktop): improve native module rebuild process by checking for runtime package.json
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 11:28:44 +05:30
Sparsh
610a102397 feat: add validation job to GitHub Actions for desktop packaging
- Introduced a new job `validate` in the GitHub Actions workflow to validate the desktop package.
- Added steps for checking out the repository, setting up Node.js, installing dependencies, type checking, and running unit tests.
- Updated the `package` job to depend on the `validate` job.

chore: update package-lock.json and package.json for vitest integration

- Added `vitest` as a development dependency in `package.json`.
- Updated `typecheck` script to include a new TypeScript configuration for vitest.
- Updated `package-lock.json` to reflect the new dependencies.

refactor: modularize runtime path handling in main process

- Created a new module `runtime-paths.ts` to handle static path normalization and request handling.
- Refactored `main.ts` to utilize the new `normalizeStaticPath`, `getRequestedPath`, and `getPackagedRendererEntry` functions.

test: add unit tests for runtime path utilities

- Implemented tests for `normalizeStaticPath`, `getRequestedPath`, and `getPackagedRendererEntry` using Vitest.
- Ensured coverage for directory traversal attempts and null-byte paths.

chore: configure Vitest for testing

- Added `vitest.config.ts` for Vitest configuration.
- Created `tsconfig.vitest.json` to include types for Vitest in TypeScript compilation.

Co-authored-by: Copilot <copilot@github.com>
2026-04-24 13:45:23 +05:30
Sparsh
1274b4bc99 feat(desktop): update dependency caching and installation steps for improved build process 2026-04-22 14:35:31 +05:30
Sparsh
7c4e8ecf19 feat(desktop): add native module rebuilding for Electron and handle startup failures 2026-04-22 13:07:52 +05:30
Sparsh
9ee1d978d9 fix(build): replace execSync with execFileSync for TypeScript compilation 2026-04-22 07:50:58 +05:30
Sparsh
fff89fb272 fix(desktop): update server health URLs to point to the info endpoint 2026-04-21 21:37:29 +05:30
Sparsh
ed83fd3e42 feat(desktop): enhance packaging workflow and add smoke test for desktop app 2026-04-21 19:24:21 +05:30
Sparsh
558d7c3d82
Merge branch 'main' into feat/Desktop-app 2026-04-21 07:20:28 +05:30
Sparsh
89f038fc67 fix(desktop): ensure-gitnexus-runtime: stop stale backend, repair runtime deps 2026-04-21 00:59:53 +05:30
evolution
c0ebd160c2
fix(docker): copy gitnexus/package.json into web builder stage (#997)
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
vite.config.ts reads engines.node from ../gitnexus/package.json,
but Dockerfile.web only copied gitnexus-shared and gitnexus-web,
causing the build to fail with "Cannot find module" during
`npm run build --prefix gitnexus-web`.

Co-authored-by: wangjichao <wangjichao@inke.cn>
2026-04-20 19:13:50 +01:00
evolution
2ac1baf450
fix(docker): use inputs.tag to detect workflow_call context (#996)
In a reusable workflow, github.event_name inherits the caller's
event (e.g. "push"), not "workflow_call". This caused the
type=raw tag to be disabled when docker.yml was called from
release-candidate.yml, producing no Docker tags at all and
failing the build.

Fix: check `inputs.tag != ''` instead, since inputs.tag is only
populated for workflow_call invocations.

Co-authored-by: wangjichao <wangjichao@inke.cn>
2026-04-20 18:20:57 +01:00
Jonas Vanderhaegen
06967e2b66
feat(extractors): add PHP HTTP consumer detection (#993)
Extend the PHP tree-sitter plugin to emit consumer HttpDetections for
three common PHP HTTP call shapes, matching Node plugin parity:

  - Laravel HTTP client:  Http::get/post/put/delete/patch($url)
  - Guzzle / generic:     $client->get/post/...($url)
  - file_get_contents($url) when the URL is absolute http(s)://

String-literal URLs only. Paths built via binary concatenation
(`$base . '/path'`), sprintf, or config lookups are intentionally
deferred — they need constant-folding of the enclosing scope to be
useful and are tracked as follow-up work.

Refs #992

Co-authored-by: Jonas Vanderhaegen <jonasvanderh+claude.ai@gmail.com>
2026-04-20 17:35:31 +01:00
Sparsh
a84380a7a5 fix(desktop): validate unpacked app startup in packaging flow 2026-04-20 21:58:01 +05:30
Sam Fakhreddine
c24bcc3bf1
fix: expose detect-changes in direct CLI (#892)
Squashed commits:
- test: fix risk_level mock case and prettier formatting in tool-direct-cli.test
- test: add edge-case coverage for detectChangesCommand formatter
2026-04-20 17:12:25 +01:00
jisue0224
8f41a1ba17
fix(bm25): return FTS-matched symbols instead of arbitrary LIMIT 3 nodes (#806)
* fix(bm25): return FTS-matched symbols instead of arbitrary LIMIT 3 nodes

Previously, bm25Search fetched up to 3 arbitrary symbols from the matched
file using MATCH (n) WHERE n.filePath = $filePath LIMIT 3 (no ORDER BY).
This meant the specific function or class that actually scored highest in
the BM25 index could be completely absent from the results.

Fix: propagate nodeId from each FTS hit through searchFTSFromLbug, then
use those nodeIds in bm25Search to look up the exact matched nodes via
WHERE n.id IN $nodeIds. Falls back to the old filePath-based lookup when
nodeIds are unavailable.

Also switches the per-file score aggregation from naive sum-of-all to
sum-of-top-3, which prevents files with many mediocre matches (e.g. test
files) from outranking files with a single highly-relevant symbol.

* test(bm25): add unit tests for top-3 aggregation and nodeIds propagation

Covers the new logic paths added in the previous commit:
- top-3 score aggregation (file with 5+ matches → only top-3 contribute)
- nodeIds propagation through BM25SearchResult
- empty nodeId filtering
- cross-table merge for the same file
- result ranking by aggregated score

Also fixes in-place entries.sort() mutation (bm25-index.ts:125) to use
[...entries].sort() so the Map value is not silently modified.

* style: apply prettier formatting

* fix(test): use importOriginal to avoid missing export errors in vi.mock

* fix(bm25): align queryFTSViaExecutor nodeId extraction to match lbug-adapter

Use node.nodeId || node.id || '' in queryFTSViaExecutor to match the
fallback logic in lbug-adapter.ts:1040. Without this, the MCP pool path
could silently return empty nodeIds if LadybugDB surfaces the node id
under node.nodeId rather than node.id.

---------

Co-authored-by: jisue0224 <>
2026-04-20 17:06:58 +01:00
xiaohaoxing
d858746476
fix(embeddings): replace recursive AST traversal with iterative DFS (#990)
findFunctionNode and findDeclarationNode had no depth limit, causing
stack overflow on deeply nested or auto-generated ASTs, especially
when --stack-size is not applied (e.g. heap already large enough to
skip ensureHeap re-exec).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:07:19 +01:00
ivkond
00966630c4
feat: cross-repo impact analysis (#794) — @repo MCP routing + group resources (#984) 2026-04-20 11:55:07 +01:00
Sparsh
c5238debf6 fix(desktop): correct packaged shell assets 2026-04-20 15:26:34 +05:30
Andrew Barnes
5c3f56df7d
docs: add --skip-git to CLI command list (#750) 2026-04-20 09:26:26 +01:00
Gergő Magyar
f53e282026
Update Discord link in README.md 2026-04-20 08:59:50 +01:00
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
Sparsh
21fb1b9a0c style(desktop): format gitnexus-desktop package 2026-04-20 10:13:29 +05:30
Sparsh
6c644e3b17 Merge branch 'main' of https://github.com/prajapatisparsh/GitNexus into feat/Desktop-app 2026-04-19 23:12:07 +05:30
Sparsh
b95eebca2e fix(desktop): restore native OS window controls (minimize/maximize/close) 2026-04-19 22:47:48 +05:30
Sparsh
31755de553 wired createserver 4747 into main.ts added scripts added predev hook 2026-04-19 15:22:05 +05:30
Sparsh
f5c6fd1b40 clean setup and ckeleton created at gitnexus-desktop 2026-04-19 14:38:36 +05:30
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
Sparsh
cbebb2c31c Basic tool setup for development 2026-04-19 13:42:15 +05:30
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