Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
- name: Unit tests
run: npm test

- name: Verify résumé PDF payloads and Hire links
run: node scripts/check-resume-assets.mjs

- name: Translation parity
run: npm run untranslated

Expand Down
36 changes: 36 additions & 0 deletions docs/reviews/2026-10-13-resume-hotfix-editorial-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Resume PDF availability hotfix — source and editorial gate (2026-10-13 PT)

**Branch**: `fix/resume-public-404-20261013`, created from public `origin/main` without altering either ongoing portfolio PR #33 / #34 or the original dirty MAIN portfolio working tree. No package version or security-gate changes.

## Public issue

As checked 2026-10-13 and again 2026-10-13 evening, all three expected public résumé filenames returned HTTP 404. Public GitHub main did not ship these files *or any Hire-page PDF download buttons*. The complete next-version branch had already prepared three dated and byte-verified PDF candidates, but their inclusion in an unmerged, security-blocked PR does not make them publicly accessible.

This independent hotfix restores the expected static paths in `public/resume/`, adds three clearly labeled and keyboard-accessible EN/KO PDF download buttons to the Hire header, and adds a **non-optional** CI step verifying each static file exists, begins with `%PDF-`, matches its recorded SHA-256, is plausibly sized and is referenced from Hire.

The PDFs are copied byte-for-byte from the evidence-validated candidate. They were generated Sep 22, 2026, are **two pages each**, unencrypted, and were rendered and examined. Their visible contents do not include a home address, SSN, date of birth, EIN or unrelated private account identifiers. They do contain a personal contact email and career details intended for public résumé distribution.

| File | Bytes | SHA-256 |
|---|---:|---|
| Sihyeong-Lee-Resume.pdf | 87672 | ab4933e15c7516cf455d0547daf4ba4b766c8d1d06a4fddfd8b4ab50a28b6b64 |
| Sihyeong-Lee-Resume-DevEx.pdf | 84927 | 9850dc45ed639d0e0998876ea2983bd3ee092c6f88ff45d3a62a9f30f3c4449f |
| Sihyeong-Lee-Resume-FDE.pdf | 88049 | 406ab1da5b66058b831530ad3b99d46a4299586207a586665e7aba5db9919350 |

## Public-content editorial approval still needed before merge

These are factual claims, not automatically established by `npm test`:
- Founder's legal employer/title/date (Soursea LLC: Nov 2025–present).
- CTO company/date timeline (Bitnara/ITEMBOX: Jun 2024–Oct 2025; GEOMEC: Feb 2021–May 2024), team size, service/module/endpoints counts and billing claims.
- DevDeck published-ci test count is a **historical 1,257** in the PDF. Reconcile with current public release vs unreleased 1.37.8 candidate rather than presenting an old number as today's passing CI count.
- Role-specific positioning, citizenship/work authorization phrasing, availability, LinkedIn/email and project release status.
- Pdf text URLs are ordinary printed strings; PDF-level clickable annotations were not present in the inspected sources. Do not falsely claim the PDF itself provides working embedded clickable links.

The owner or authorized editor must approve the **exact 3 PDF contents** before a public production merge. This PR is a draft review candidate and does not constitute approval. If a correction is requested, regenerate the exact affected PDF, update SHA-256 in `scripts/check-resume-assets.mjs`, rerender and repeat release QA.

## Acceptance / rollback

1. `npm ci`, `node scripts/check-resume-assets.mjs`, type/lint/format/unit/translation/build must pass without relaxing `npm audit`.
2. GitHub Linux CI and Vercel preview: inspect EN/KO Hire first viewport and three PDF routes. Confirm public preview HTTP 200 `application/pdf`, page count and **byte-identical SHA-256** to reviewed source.
3. Record exact public-content approval. Confirm security gate for current production baseline separately (this PR changes no dependencies).
4. Merge through normal GitHub review, verify all three public URLs and both Hire CTA links in live production, check source deployment hash. If the deployment creates regression, revert the narrow commit/deployment without changing other production services.
5. Keep FOLIO-51 and FOLIO-36 open until live evidence exists. This doc is a source handoff, not a successful deployment claim.
Binary file added public/resume/Sihyeong-Lee-Resume-DevEx.pdf
Binary file not shown.
Binary file added public/resume/Sihyeong-Lee-Resume-FDE.pdf
Binary file not shown.
Binary file added public/resume/Sihyeong-Lee-Resume.pdf
Binary file not shown.
41 changes: 41 additions & 0 deletions scripts/check-resume-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createHash } from 'node:crypto'
import { existsSync, readFileSync, statSync } from 'node:fs'
import { resolve } from 'node:path'

/**
* Candidate résumé-release invariant.
* These are SHA-256 fingerprints of the three reviewed September 22 PDF
* artifacts; a regeneration requires updating this manifest in the same PR.
*/
const resumes = [
{
file: 'Sihyeong-Lee-Resume.pdf',
sha256: 'ab4933e15c7516cf455d0547daf4ba4b766c8d1d06a4fddfd8b4ab50a28b6b64',
},
{
file: 'Sihyeong-Lee-Resume-DevEx.pdf',
sha256: '9850dc45ed639d0e0998876ea2983bd3ee092c6f88ff45d3a62a9f30f3c4449f',
},
{
file: 'Sihyeong-Lee-Resume-FDE.pdf',
sha256: '406ab1da5b66058b831530ad3b99d46a4299586207a586665e7aba5db9919350',
},
]

const hirePage = readFileSync(resolve('src/app/[locale]/hire/page.tsx'), 'utf8')
const checks = []

for (const { file, sha256 } of resumes) {
const path = resolve('public/resume', file)
if (!existsSync(path)) throw new Error(`Résumé PDF missing: ${file}`)
const bytes = readFileSync(path)
const hash = createHash('sha256').update(bytes).digest('hex')
if (!bytes.subarray(0, 5).equals(Buffer.from('%PDF-'))) throw new Error(`Invalid PDF header: ${file}`)
if (hash !== sha256) throw new Error(`Résumé PDF SHA mismatch: ${file}`)
if (!hirePage.includes(file)) throw new Error(`Hire page has no path to ${file}`)
if (statSync(path).size < 25_000) throw new Error(`Résumé PDF truncated: ${file}`)
checks.push({ file, sha256, bytes: bytes.length })
}

console.log('PASS all 3 résumé PDF payloads and Hire CTAs')
for (const item of checks) console.log(`- ${item.file}: ${item.bytes} bytes, SHA-256 ${item.sha256.slice(0, 12)}…`)
37 changes: 37 additions & 0 deletions src/app/[locale]/hire/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ function OfferRow({ label, children }: { label: string; children: ReactNode }) {
)
}

const RESUME_OPTIONS = [
{
file: 'Sihyeong-Lee-Resume.pdf',
en: 'General / AI product engineering',
ko: '일반 · AI 제품 엔지니어링',
},
{
file: 'Sihyeong-Lee-Resume-DevEx.pdf',
en: 'Developer experience',
ko: '개발자 경험 · DevEx',
},
{
file: 'Sihyeong-Lee-Resume-FDE.pdf',
en: 'Forward-deployed engineering',
ko: '현장형 · Forward-deployed',
},
] as const

function HireContent() {
const t = useTranslations('hire')
const locale = useLocale()
Expand Down Expand Up @@ -112,6 +130,25 @@ function HireContent() {
</span>
))}
</div>
<nav
className="mt-5 flex flex-wrap items-center gap-2"
aria-label={locale === 'ko' ? '이력서 PDF 다운로드' : 'Download résumés'}
>
{RESUME_OPTIONS.map((option, index) => (
<a
key={option.file}
href={`/resume/${option.file}`}
download={option.file}
className={`inline-flex min-h-11 items-center rounded-md border px-3.5 py-2.5 text-xs font-medium transition-opacity hover:opacity-85 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--accent-text)] ${
index === 0
? 'border-[var(--accent-text)] bg-[var(--accent-text)] text-[var(--bg-primary)]'
: 'border-[var(--border-hover)] text-[var(--text-primary)]'
}`}
>
{locale === 'ko' ? option.ko : option.en} PDF ↓
</a>
))}
</nav>
</header>

{/* Two offers, stated separately. A merged "open to opportunities"
Expand Down
Loading