> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/clyrisai/gitresolve/llms.txt
> Use this file to discover all available pages before exploring further.

# resolveOwnerAndCategorize — Owner Disambiguation Reference

> Determine which git profile belongs to the candidate from a list of ExtractedGitLinks, with confidence scoring and repo categorization.

The disambiguator takes a flat list of `ExtractedGitLink` objects — profiles, repos, PRs, and issues — and works out which profile is the candidate's own. It then categorises every repo link as either owned by the candidate or external. `scrapePortfolio` and `parseResume` both call this internally, but you can invoke it directly when you have already collected links from a custom source.

## `resolveOwnerAndCategorize`

Determines the candidate owner from a list of parsed git links and splits repo links into owned, contributed, and external buckets.

```typescript theme={null}
function resolveOwnerAndCategorize(
  links: ExtractedGitLink[],
  sourceContext?: string,
  knownOwnerProfile?: ExtractedGitLink
): {
  ownerProfile: ExtractedGitLink | null;
  confidence: 'high' | 'medium' | 'low' | 'none';
  ownedRepos: ExtractedGitLink[];
  contributions: ExtractedGitLink[];
  externalRepos: ExtractedGitLink[];
  warnings: string[];
}
```

### Parameters

<ParamField path="links" type="ExtractedGitLink[]" required>
  The full list of parsed git links from a single source. Usually the `allLinks` array produced by `scrapePortfolio` or `parseResume`. May be empty — the function handles the empty case by returning `confidence: 'none'`.
</ParamField>

<ParamField path="sourceContext" type="string">
  An optional hint string used in warning messages to indicate where the links came from (e.g. `'portfolio'`, `'resume'`). Does not affect the resolution algorithm.
</ParamField>

<ParamField path="knownOwnerProfile" type="ExtractedGitLink">
  If you already know the candidate's git profile (from a different source, or from a direct input), supply it here. When provided, the disambiguation algorithm is bypassed entirely — `ownerProfile` is set to this value, `confidence` is forced to `'high'`, and a warning is added noting the bypass.
</ParamField>

### Returns

An object with the following shape. The underlying `OwnerResolution` interface is internal to the library — it is **not** exported from `@clyrisai/gitresolve` and cannot be imported by name. Use the field descriptions below as your type reference.

<ResponseField name="ownerProfile" type="ExtractedGitLink | null">
  The resolved candidate git profile, or `null` when no owner could be determined (Case 4).
</ResponseField>

<ResponseField name="confidence" type="'high' | 'medium' | 'low' | 'none'">
  How confident the algorithm is in the resolved owner. See the four cases below.
</ResponseField>

<ResponseField name="ownedRepos" type="ExtractedGitLink[]">
  Repo links (type `'repo'`) where `username` matches the resolved owner, case-insensitively. Duplicates (same URL) are removed.
</ResponseField>

<ResponseField name="contributions" type="ExtractedGitLink[]">
  All `pull_request` and `issue` links from the input, deduplicated by URL.
</ResponseField>

<ResponseField name="externalRepos" type="ExtractedGitLink[]">
  Repo links whose `username` does not match the resolved owner — third-party repos referenced on the page or in the document.
</ResponseField>

<ResponseField name="warnings" type="string[]">
  Diagnostic strings describing how the owner was determined (or why it could not be).
</ResponseField>

### `knownOwnerProfile` bypass

When `knownOwnerProfile` is supplied, the function skips all four disambiguation cases. The owner is accepted as given, `confidence` is set to `'high'`, and the following warning is added:

```
Owner strictly determined by profile URL input: {username}
```

Repo categorisation still runs normally — `ownedRepos` and `externalRepos` are computed relative to the supplied username.

### The four disambiguation cases

<Expandable title="Case 1 — Exactly one unique profile username (HIGH confidence)">
  When the deduplicated profile links resolve to exactly one unique username (case-insensitive), that profile is the owner. This is the most common and most reliable outcome.

  **Confidence:** `high`

  **Example:** The portfolio page has a single `https://github.com/janedoe` link and several `github.com/janedoe/...` repo links. Owner = `janedoe`.

  ```typescript theme={null}
  const links = [
    { type: 'profile', username: 'janedoe', provider: 'github', url: 'https://github.com/janedoe' },
    { type: 'repo',    username: 'janedoe', provider: 'github', url: 'https://github.com/janedoe/my-app', repo: 'my-app' },
  ];

  const result = resolveOwnerAndCategorize(links);
  // result.confidence === 'high'
  // result.ownerProfile.username === 'janedoe'
  ```
</Expandable>

<Expandable title="Case 2 — Multiple distinct profile usernames (HIGH or MEDIUM or LOW confidence)">
  When multiple unique profile usernames are found (common on portfolio pages that link to collaborators), the algorithm cross-references each profile's username against the repo links.

  **Sub-case 2a — Clear majority (HIGH):** The top-scoring profile owns strictly more repos than the second-place profile. The top scorer is the owner.

  **Sub-case 2b — Tied repo ownership (MEDIUM):** Two or more profiles have an equal number of matching repos. The first one in the list is picked and flagged.

  **Sub-case 2c — No repos match any profile (LOW):** All repo links belong to usernames that don't match any profile link. The first profile is used with a warning.

  ```typescript theme={null}
  const links = [
    { type: 'profile', username: 'janedoe',   ... },
    { type: 'profile', username: 'collaborator', ... },
    { type: 'repo',    username: 'janedoe',   repo: 'project-a', ... },
    { type: 'repo',    username: 'janedoe',   repo: 'project-b', ... },
    { type: 'repo',    username: 'collaborator', repo: 'their-lib', ... },
  ];

  const result = resolveOwnerAndCategorize(links);
  // result.confidence === 'high' — janedoe has 2 repos vs collaborator's 1
  // result.ownerProfile.username === 'janedoe'
  // result.ownedRepos.length === 2
  // result.externalRepos.length === 1
  ```
</Expandable>

<Expandable title="Case 3 — No profile links, only repos (MEDIUM or LOW confidence)">
  When there are no profile links at all, the algorithm infers the owner from the repo links by grouping repos by username and picking the most frequent.

  A synthetic profile link is constructed: `https://{host}/{topUsername}`

  **Sub-case 3a — All repos belong to one username, or one username has a clear majority (MEDIUM):** The majority owner is used.

  **Sub-case 3b — Tied repo usernames (LOW):** The first (alphabetically sorted by frequency, then insertion order) username is picked and flagged.

  ```typescript theme={null}
  const links = [
    { type: 'repo', username: 'janedoe', repo: 'project-a', ... },
    { type: 'repo', username: 'janedoe', repo: 'project-b', ... },
  ];

  const result = resolveOwnerAndCategorize(links);
  // result.confidence === 'medium'
  // result.ownerProfile.url === 'https://github.com/janedoe' (synthetic)
  // result.warnings includes 'No profile link found — all repos belong to same user...'
  ```
</Expandable>

<Expandable title="Case 4 — Nothing found (NONE confidence)">
  When the input `links` array is empty, no resolution is possible.

  ```typescript theme={null}
  const result = resolveOwnerAndCategorize([]);
  // result.ownerProfile === null
  // result.confidence === 'none'
  // result.warnings === ['No git links found']
  ```
</Expandable>

### Repo categorisation rules

After the owner is determined, every link in the `links` array is assigned to exactly one bucket:

| Bucket          | Condition                                                                                |
| --------------- | ---------------------------------------------------------------------------------------- |
| `ownedRepos`    | `link.type === 'repo'` AND `link.username.toLowerCase() === ownerUsername.toLowerCase()` |
| `externalRepos` | `link.type === 'repo'` AND username does NOT match owner                                 |
| `contributions` | `link.type === 'pull_request'` OR `link.type === 'issue'`                                |

Profile links, gist links, and `'other'` type links are not placed in any of the three buckets but remain in the input `links` array (accessible as `allLinks` on `ResolverResult`).

Duplicate repo URLs (case-insensitive) are deduplicated across `ownedRepos` and `externalRepos` — the first occurrence is kept.

### When to call directly

In most cases you do not need to call `resolveOwnerAndCategorize` yourself — `scrapePortfolio` and `parseResume` call it internally. You should call it directly when:

* You have collected `ExtractedGitLink` objects from a custom source not covered by the built-in functions.
* You want to re-run disambiguation on a combined link list from multiple sources (e.g. merging `allLinks` from a portfolio result and a resume result).
* You are testing your own parsing logic and want to verify categorisation.

### Examples

<CodeGroup>
  ```typescript Basic usage theme={null}
  import { resolveOwnerAndCategorize } from '@clyrisai/gitresolve';
  import type { ExtractedGitLink } from '@clyrisai/gitresolve';

  const links: ExtractedGitLink[] = [
    {
      url: 'https://github.com/janedoe',
      provider: 'github',
      type: 'profile',
      username: 'janedoe',
    },
    {
      url: 'https://github.com/janedoe/api-service',
      provider: 'github',
      type: 'repo',
      username: 'janedoe',
      repo: 'api-service',
    },
    {
      url: 'https://github.com/facebook/react',
      provider: 'github',
      type: 'repo',
      username: 'facebook',
      repo: 'react',
    },
  ];

  const resolution = resolveOwnerAndCategorize(links, 'custom');

  console.log(resolution.ownerProfile?.username);  // 'janedoe'
  console.log(resolution.confidence);              // 'high'
  console.log(resolution.ownedRepos.length);       // 1
  console.log(resolution.externalRepos.length);    // 1 (facebook/react)
  ```

  ```typescript Merging links from multiple sources theme={null}
  import {
    resolveOwnerAndCategorize,
    parseResume,
    scrapePortfolio,
    createProvider,
  } from '@clyrisai/gitresolve';

  const provider = await createProvider();

  try {
    const [portfolio, resume] = await Promise.all([
      scrapePortfolio('https://janedoe.dev', provider),
      parseResume('./resumes/janedoe.pdf'),
    ]);

    // Combine all links from both sources and re-resolve
    const combinedLinks = [
      ...portfolio.allLinks,
      ...resume.allLinks,
    ];

    const resolution = resolveOwnerAndCategorize(combinedLinks, 'combined');

    console.log('Combined owner:', resolution.ownerProfile?.username);
    console.log('Confidence:', resolution.confidence);
    console.log('Total owned repos:', resolution.ownedRepos.length);
  } finally {
    await provider.cleanup();
  }
  ```

  ```typescript No profiles — inferring from repos theme={null}
  import { resolveOwnerAndCategorize } from '@clyrisai/gitresolve';
  import type { ExtractedGitLink } from '@clyrisai/gitresolve';

  // Resume only listed repo URLs, no profile link
  const links: ExtractedGitLink[] = [
    { url: 'https://github.com/janedoe/project-a', provider: 'github', type: 'repo', username: 'janedoe', repo: 'project-a' },
    { url: 'https://github.com/janedoe/project-b', provider: 'github', type: 'repo', username: 'janedoe', repo: 'project-b' },
    { url: 'https://github.com/janedoe/project-c', provider: 'github', type: 'repo', username: 'janedoe', repo: 'project-c' },
  ];

  const resolution = resolveOwnerAndCategorize(links);

  console.log(resolution.confidence);                 // 'medium'
  console.log(resolution.ownerProfile?.url);          // 'https://github.com/janedoe' (synthetic)
  console.log(resolution.warnings);
  // ['No profile link found — all repos belong to same user, inferred as owner']
  ```
</CodeGroup>

***

## `dedupeProfilesByUsername`

Removes duplicate profile links from an array, keeping the first occurrence of each unique username. The comparison is case-insensitive so `JaneDoe` and `janedoe` are treated as the same person.

```typescript theme={null}
function dedupeProfilesByUsername(profiles: ExtractedGitLink[]): ExtractedGitLink[]
```

### Parameters

<ParamField path="profiles" type="ExtractedGitLink[]" required>
  An array of `ExtractedGitLink` objects. Only `username` is used for deduplication — `url`, `provider`, and other fields are not compared.
</ParamField>

### Returns

A new array containing only the first occurrence of each unique lowercased username. The original array is not mutated. Non-profile link types are passed through unchanged since only `username` is used as the dedup key.

### Example

```typescript theme={null}
import { dedupeProfilesByUsername } from '@clyrisai/gitresolve';
import type { ExtractedGitLink } from '@clyrisai/gitresolve';

const profiles: ExtractedGitLink[] = [
  { url: 'https://github.com/JaneDoe',   provider: 'github', type: 'profile', username: 'JaneDoe' },
  { url: 'https://github.com/janedoe',   provider: 'github', type: 'profile', username: 'janedoe' },
  { url: 'https://gitlab.com/janedoe',   provider: 'gitlab', type: 'profile', username: 'janedoe' },
  { url: 'https://github.com/bobsmith',  provider: 'github', type: 'profile', username: 'bobsmith' },
];

const unique = dedupeProfilesByUsername(profiles);
// [
//   { username: 'JaneDoe', url: 'https://github.com/JaneDoe', ... },  ← first occurrence kept
//   { username: 'bobsmith', url: 'https://github.com/bobsmith', ... },
// ]
// 'janedoe' (github) and 'janedoe' (gitlab) are both dropped as duplicates
```
