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.