> ## 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.

# parseResume — Local PDF Resume Parser API Reference

> Parse a local PDF resume with dual-method extraction (text layer + hyperlink annotations) and receive a structured ResolverResult with git profiles.

The resume parser reads a PDF file from the local filesystem and extracts every git provider URL it can find using two independent methods: rendered text extraction and hyperlink annotation scanning. Both methods run independently so that a failure in one does not block the other. The result is the same `ResolverResult` shape returned by `scrapePortfolio`, making it straightforward to combine portfolio and resume data downstream.

## `parseResume`

Reads a local PDF file and resolves any git profile or repo links it contains into a structured `ResolverResult`.

```typescript theme={null}
async function parseResume(filePath: string): Promise<ResolverResult>
```

### Parameters

<ParamField path="filePath" type="string" required>
  Path to a local PDF file. Both relative paths (`./resumes/janedoe.pdf`) and absolute paths (`/tmp/uploads/janedoe.pdf`) are accepted. The path is passed directly to Node.js `fs/promises.readFile`.
</ParamField>

<Note>
  `parseResume` only accepts **local file paths**. It cannot fetch remote PDFs over HTTP. If you have a PDF URL, download the file first and then pass the local path to `parseResume`.
</Note>

### Returns

`Promise<ResolverResult>` — this function **never throws**. All errors are surfaced inside the result object.

<ResponseField name="source" type="string">
  The `filePath` argument, unchanged.
</ResponseField>

<ResponseField name="sourceType" type="'resume_file'">
  Always `'resume_file'` for results from this function.
</ResponseField>

<ResponseField name="ownerProfile" type="ExtractedGitLink | null">
  The resolved candidate git profile, or `null` if none could be determined from the resume.
</ResponseField>

<ResponseField name="confidence" type="'high' | 'medium' | 'low' | 'none'">
  Confidence level from the owner disambiguation step.
</ResponseField>

<ResponseField name="ownedRepos" type="ExtractedGitLink[]">
  Repos whose owner username matches the resolved candidate username (case-insensitive).
</ResponseField>

<ResponseField name="contributions" type="ExtractedGitLink[]">
  Explicit PR and issue links found in the resume.
</ResponseField>

<ResponseField name="externalRepos" type="ExtractedGitLink[]">
  Repos referenced in the resume but owned by a different username.
</ResponseField>

<ResponseField name="allLinks" type="ExtractedGitLink[]">
  Every `ExtractedGitLink` parsed from the resume, before categorisation.
</ResponseField>

<ResponseField name="warnings" type="string[]">
  Diagnostic messages. Extraction method failures appear here (e.g. `"Text extraction failed: ..."` or `"Annotation extraction failed: ..."`). Also includes counts and disambiguator messages.
</ResponseField>

<ResponseField name="error" type="string | undefined">
  Set **only** when `fs.readFile` itself fails (file not found, permission denied, etc.). Individual extraction method failures are in `warnings`, not `error`.
</ResponseField>

### How it works

<Steps>
  <Step title="Read the file">
    Calls `fs.readFile(filePath)` to load the entire PDF into a `Buffer`. If this fails (file not found, permission error), `result.error` is set and the function returns immediately with empty arrays.
  </Step>

  <Step title="Method 1 — Text layer extraction">
    Dynamically imports `unpdf` and calls `getDocumentProxy` + `extractText` with `mergePages: true`. The resulting text string is passed to `extractGitUrlsFromText`, which regex-scans for all git provider URLs. Failures are caught and appended to `result.warnings` without affecting Method 2.
  </Step>

  <Step title="Method 2 — Hyperlink annotation extraction">
    Creates a second independent copy of the PDF buffer (to avoid shared-state issues) and iterates every page via `pdf.getPage(i)` + `page.getAnnotations()`. Any annotation of `subtype: 'Link'` with a `url` string is checked against `GIT_HOSTS`. Matching annotation URLs are collected. Failures are caught and appended to `result.warnings`.
  </Step>

  <Step title="Deduplicate">
    Both URL sets are merged, trailing slashes are stripped, and the combined list is deduplicated with `Set`. This handles the common case where a git URL appears as both visible text and a clickable hyperlink.
  </Step>

  <Step title="Parse and classify">
    Each unique URL is passed to `parseGitLink`. Results that are not `null` are added to `result.allLinks`.
  </Step>

  <Step title="Resolve owner and categorise">
    `resolveOwnerAndCategorize(result.allLinks, 'resume')` runs the full disambiguation logic and populates `ownerProfile`, `confidence`, `ownedRepos`, `contributions`, and `externalRepos`.
  </Step>
</Steps>

### Error vs warnings

| Scenario                              | Where it appears                             |
| ------------------------------------- | -------------------------------------------- |
| File not found / permission denied    | `result.error`                               |
| `unpdf` text extraction threw         | `result.warnings`                            |
| Annotation iteration threw            | `result.warnings`                            |
| Disambiguator could not find an owner | `result.warnings` (and `confidence: 'none'`) |

<Tip>
  Always check `result.warnings` even on success. A warning like `"Text extraction failed: ..."` means only the annotation method ran — the result may be less complete than expected.
</Tip>

### Examples

<CodeGroup>
  ```typescript Single resume theme={null}
  import { parseResume } from '@clyrisai/gitresolve';

  const result = await parseResume('./resumes/janedoe.pdf');

  if (result.error) {
    console.error('Could not read file:', result.error);
  } else {
    console.log('Owner profile:', result.ownerProfile?.url);
    console.log('Confidence:',    result.confidence);
    console.log('Owned repos:',   result.ownedRepos.map(r => r.repo));
    console.log('Contributions:', result.contributions.length);
    console.log('Warnings:',      result.warnings);
  }
  ```

  ```typescript Multiple resumes in parallel theme={null}
  import { parseResume } from '@clyrisai/gitresolve';
  import { readdir } from 'fs/promises';
  import { join } from 'path';

  const resumeDir = './resumes';
  const files = (await readdir(resumeDir)).filter(f => f.endsWith('.pdf'));

  const results = await Promise.all(
    files.map(f => parseResume(join(resumeDir, f)))
  );

  for (const result of results) {
    const username = result.ownerProfile?.username ?? 'unresolved';
    const confidence = result.confidence;
    console.log(`${result.source}: ${username} (${confidence})`);

    if (result.warnings.length > 0) {
      console.warn('  Warnings:', result.warnings.join('; '));
    }
  }
  ```

  ```typescript Remote PDF — download first theme={null}
  import { parseResume } from '@clyrisai/gitresolve';
  import { writeFile, unlink } from 'fs/promises';
  import { tmpdir } from 'os';
  import { join } from 'path';

  async function parseRemotePdf(pdfUrl: string) {
    const response = await fetch(pdfUrl);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const buffer = Buffer.from(await response.arrayBuffer());
    const tmpPath = join(tmpdir(), `resume-${Date.now()}.pdf`);

    await writeFile(tmpPath, buffer);

    try {
      return await parseResume(tmpPath);
    } finally {
      await unlink(tmpPath).catch(() => {});
    }
  }

  const result = await parseRemotePdf('https://example.com/candidates/janedoe.pdf');
  console.log('Owner:', result.ownerProfile?.username);
  ```

  ```typescript Combining with portfolio result theme={null}
  import { createProvider, scrapePortfolio, parseResume } from '@clyrisai/gitresolve';

  const provider = await createProvider();

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

    // Both resolve to the same candidate — pick the higher-confidence result
    const primary = portfolio.confidence === 'high' ? portfolio : resume;
    console.log('Primary owner:', primary.ownerProfile?.username);

    // Merge owned repos from both sources
    const allOwnedRepos = [
      ...portfolio.ownedRepos,
      ...resume.ownedRepos,
    ].filter((r, i, arr) =>
      arr.findIndex(x => x.url === r.url) === i
    );
    console.log('Total unique owned repos:', allOwnedRepos.length);
  } finally {
    await provider.cleanup();
  }
  ```
</CodeGroup>
