diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b547964..0a999d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: - name: Unit tests run: npm test + - name: Evidence and résumé integrity + run: npm run evidence:check + - name: Translation parity run: npm run untranslated @@ -47,6 +50,36 @@ jobs: - name: Build run: npm run build + - name: Install browser for release QA + run: npx playwright install --with-deps chromium + + - name: Production preview browser QA + shell: bash + run: | + mkdir -p .qa/portfolio + npm start -- -p 3100 -H localhost > .qa/portfolio/server.log 2>&1 & + PREVIEW_PID=$! + trap 'kill "$PREVIEW_PID" 2>/dev/null || true' EXIT + ready=0 + for attempt in $(seq 1 40); do + if curl -fsS http://localhost:3100/en/hire > /dev/null; then ready=1; break; fi + sleep 1 + done + test "$ready" = 1 + npm run qa:portfolio + npm run qa:navigation + QA_LOCALE=ko npm run qa:navigation + + - name: Preserve browser QA evidence + if: always() + uses: actions/upload-artifact@v7 + with: + name: portfolio-browser-qa + path: .qa/portfolio/ + include-hidden-files: true + retention-days: 14 + if-no-files-found: warn + # Always check production independently so a dev-tool advisory cannot # conceal any newly introduced runtime vulnerability. - name: Audit production dependencies diff --git a/.gitignore b/.gitignore index 5815c74..a83f63c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ next-env.d.ts # local agent permissions/state .claude/settings.local.json + +# Local verification artifacts; never public content or automatic claims +/.qa/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..69d6369 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# Portfolio execution guide + +Read README.md and docs/operations/evidence-workflow.md. Preserve existing uncommitted changes. + +- Tasks: live Plane Portfolio (FOLIO), workspace soursea; project ID 1bc2719a-4d8b-4182-ac54-743acfe8d639. Read live work before implementation. Separate implementation, automated QA, visual review and deployment. +- Public evidence: content/evidence.json. Check the Git version before trusting a source README. Collection writes only ignored review candidates and never publishes measurements automatically. +- Résumé originals: notebook hangar/data. Public files are versioned snapshots; do not mix unrelated personal records into this repository. +- Core checks: npm run type-check; npm run lint; npm run format:check; npm test; npm run evidence:check; npm run untranslated; npm run build. +- Browser checks: start production preview, then npm run qa:portfolio and both locales of qa:navigation. Preserve .qa/portfolio/report.json and screenshots. Do not remove console assertions to manufacture PASS. +- Never delete Git lock files without establishing ownership. No force push, visibility change or deployment as an incidental maintenance action. +- Current design/research decisions live under docs/research and docs/reviews. Their written dates and limitations matter. diff --git a/README.md b/README.md index 990f393..dd52bda 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,18 @@ scripts/ content/media verification and capture utilities ``` The project ledger is intentionally broader than "shipped" work. UI counts distinguish total builds from public destinations and store listings rather than presenting every row as a release. + +## Evidence and release QA + +See [the evidence workflow](docs/operations/evidence-workflow.md) for reviewed source snapshots, read-only collection, résumé integrity and production-preview QA. The collector never automatically publishes a new number. + +```bash +npm run evidence:check +npm run evidence:collect +# After npm run build, start a preview in another terminal: +npm start -- -p 3100 -H localhost +# Then run the persisted browser matrix: +npm run qa:portfolio +``` + +The [AI positioning review](docs/research/ai-positioning-2026-09-22.md) separates source-backed engineering themes from unverified market claims. The [MingleStage selection decision](docs/reviews/minglestage-case-study-2026-09-22.md) keeps staged work distinct from public releases. Execution is tracked in Plane Portfolio (FOLIO); screenshots and machine reports are evidence, not automatic human approval. diff --git a/content/about.json b/content/about.json index a4fece3..9b76b2b 100644 --- a/content/about.json +++ b/content/about.json @@ -98,15 +98,15 @@ }, { "name": "Google Gemini", - "category": "tools" + "category": "ai" }, { "name": "Claude API", - "category": "tools" + "category": "ai" }, { "name": "Model Context Protocol", - "category": "tools" + "category": "ai" }, { "name": "Turborepo", @@ -135,6 +135,66 @@ { "name": "Ollama", "category": "infra" + }, + { + "name": "Claude Code", + "category": "ai" + }, + { + "name": "OpenAI / Codex", + "category": "ai" + }, + { + "name": "Agent orchestration", + "category": "ai" + }, + { + "name": "Tool calling", + "category": "ai" + }, + { + "name": "LLM eval harnesses", + "category": "ai" + }, + { + "name": "RAG", + "category": "ai" + }, + { + "name": "MongoDB", + "category": "backend" + }, + { + "name": "TypeORM", + "category": "backend" + }, + { + "name": "SQLite", + "category": "backend" + }, + { + "name": "Celery", + "category": "backend" + }, + { + "name": "PostGIS", + "category": "backend" + }, + { + "name": "AWS", + "category": "infra" + }, + { + "name": "GitHub Actions", + "category": "tools" + }, + { + "name": "pytest", + "category": "tools" + }, + { + "name": "PowerShell", + "category": "tools" } ], "timeline": [ diff --git a/content/evidence.json b/content/evidence.json new file mode 100644 index 0000000..39343b6 --- /dev/null +++ b/content/evidence.json @@ -0,0 +1,51 @@ +{ + "schemaVersion": 1, + "reviewAfter": "2026-12-22", + "records": [ + { + "id": "devdeck-maintenance", + "project": "devdeck", + "kind": "source-snapshot", + "snapshotAt": "2026-09-08", + "reviewedAt": "2026-09-22", + "titleEn": "State consistency & recovery", + "titleKo": "상태 일관성 · 복구 검증", + "value": "1,289 passed · 1 skipped", + "scopeEn": "Documented 1.37.8 candidate validation. Not a current test run or proof of release, installer acceptance, or multi-day physical-network testing.", + "scopeKo": "1.37.8 후보 버전의 문서화된 검사 결과입니다. 현재 테스트 실행·공개 출시·설치 검수·장기간 실기기 네트워크 검증을 의미하지 않습니다.", + "source": { + "repository": "writingdeveloper/devdeck", + "path": "docs/quality-review-2026-09-08.md", + "commit": "b7817547dd2100751ce46229a83810319affe82b", + "sha256": "deac81f7234aa62693414ec7fb04da8ef19200a82b0a1955fbc532423980c372" + }, + "measurements": { + "passed": 1289, + "skipped": 1 + } + }, + { + "id": "citefirst-retrieval", + "project": "citefirst", + "kind": "source-snapshot", + "snapshotAt": "2026-07-29", + "reviewedAt": "2026-09-22", + "titleEn": "Retrieval measured on one corpus", + "titleKo": "명시된 코퍼스에서 측정한 검색 품질", + "value": "recall@5 0.978 · MRR@10 0.834", + "scopeEn": "46 PostgreSQL 17 manual questions in the committed July configuration. The separate 56-question local experiment is uncommitted and is not published as this baseline. Citation-ID validity is not answer correctness.", + "scopeKo": "커밋된 7월 구성으로 PostgreSQL 17 매뉴얼 질문 46개를 평가한 기록입니다. 별도의 56문항 로컬 실험은 미커밋 상태라 이 기준값으로 공개하지 않습니다. 인용 ID 유효성은 답변 정확도와 다릅니다.", + "source": { + "repository": "writingdeveloper/citefirst", + "path": "README.md", + "commit": "bf835b6a138b36f1ddfaa5be21702645973d1293", + "sha256": "1e597f7044e288800845e2feba3ddf1ed11c7717a511d536404a40ff9b6173df" + }, + "measurements": { + "questions": 46, + "recall5": 0.978, + "mrr10": 0.834 + } + } + ] +} diff --git a/content/failure-cases.json b/content/failure-cases.json new file mode 100644 index 0000000..415a016 --- /dev/null +++ b/content/failure-cases.json @@ -0,0 +1,166 @@ +{ + "schemaVersion": 1, + "incidents": [ + { + "id": "devdeck-session-title-convergence", + "project": "devdeck", + "recordedAt": "2026-09-08", + "verifiedAt": "2026-10-12", + "baselineCommit": "b7817547dd2100751ce46229a83810319affe82b", + "candidateCommit": "fa8864833b98e7d81f140e598b1f56d4d2b60c39", + "candidateVersion": "1.37.8", + "statusEn": "Validated candidate — not a public release", + "statusKo": "검증된 후보 버전 · 정식 출시 아님", + "titleEn": "The rename reached the host. The host screen stayed wrong.", + "titleKo": "이름 변경은 호스트에 도착했지만, 호스트 화면은 그대로였다.", + "introEn": "A remote viewer could rename a coding-agent terminal and the host's runtime metadata would change, yet its visible header and sidebar stayed stale. The original check inspected internal state rather than the user-facing window or disk record. A three-app regression made the visible failure reproducible.", + "introKo": "원격 뷰어에서 코딩 에이전트 세션 이름을 바꾸면 호스트의 런타임 메타데이터는 바뀌었지만, 실제 제목과 사이드바는 이전 값을 유지했습니다. 기존 검사는 내부 상태만 확인하고 화면이나 디스크 기록의 일치를 검사하지 않았습니다. Electron 앱 3개를 실행하는 회귀 테스트로 사용자가 보던 실패를 재현했습니다.", + "before": { + "image": "/images/projects/devdeck-sync-qa-before.webp", + "altEn": "Real isolated Electron QA fixture: the baseline host header and sidebar still show release-workspace after a remote rename.", + "altKo": "실제 격리 Electron QA에서 원격 이름 변경 이후에도 호스트 제목과 사이드바에 release-workspace가 남은 과거 실패 화면.", + "titleEn": "Before — host UI still shows the old title", + "titleKo": "수정 전 — 호스트 화면에 이전 제목이 남음", + "noteEn": "Historical 2026-09-08 failed fixture. The image combines two crops from the same QA screenshot; the terminal pane containing a local path is intentionally excluded.", + "noteKo": "2026-09-08 과거 실패 기록입니다. 동일한 QA 화면에서 제목과 사이드바를 잘라 합쳤으며, 로컬 경로가 있는 터미널 영역은 제외했습니다." + }, + "after": { + "image": "/images/projects/devdeck-sync-qa-after.webp", + "altEn": "Real isolated Electron QA fixture on 2026-10-12: host header and sidebar both show the committed title Converged 5.", + "altKo": "2026-10-12 격리된 실제 Electron QA에서 호스트 제목과 사이드바가 모두 Converged 5로 일치하는 화면.", + "titleEn": "After — host UI and sidebar agree", + "titleKo": "수정 후 — 호스트 제목과 사이드바가 일치", + "noteEn": "2026-10-12 fixture after eight convergence scenarios. Crops are taken from the same capture; no production conversations or customer data were used.", + "noteKo": "2026-10-12 8개 동기화 시나리오 이후의 가상 데이터 화면입니다. 한 화면의 일부를 추출했으며 실제 사용자 대화·고객 데이터는 사용하지 않았습니다." + }, + "reproduction": { + "titleEn": "Reproduce the user's failure", + "titleKo": "사용자에게 보인 실패 재현", + "stepsEn": [ + "Launch a disposable Windows Electron host and two paired viewers, each with a separate profile; pair through real loopback TLS with a harmless fixture CLI.", + "Rename a running terminal from Viewer A. On the historical baseline, host runtime metadata changed, but the host window still rendered its old header/sidebar title.", + "Assert the actual header, sidebar, saved-session list and on-disk state on all three apps—not only the transport response.", + "The historical baseline's first UI convergence check timed out. This candidate suite passes the same class of acceptance tests." + ], + "stepsKo": [ + "서로 다른 임시 프로필로 Windows Electron 호스트 1개와 뷰어 2개를 실행하고, 가짜 CLI와 실제 루프백 TLS로 연결합니다.", + "뷰어 A에서 실행 중인 터미널 이름을 변경합니다. 과거 기준 코드에서는 호스트 런타임 상태가 바뀌어도 제목·사이드바 화면은 이전 값을 표시했습니다.", + "전송 응답만 보지 않고 세 앱의 제목·사이드바·저장 목록·디스크 저장 상태까지 확인합니다.", + "과거 기준 버전에서는 첫 화면 일치 검사에서 시간이 초과됐고, 수정 후보의 동일 유형 사용자 경험 테스트는 통과했습니다." + ] + }, + "rootCause": { + "titleEn": "Root cause — a broken source of truth", + "titleKo": "원인 — 상태의 소유권이 일관되지 않음", + "textEn": "The host renderer's syncAdoptedLabel path excluded LOCAL_MACHINE_ID announcements, treating its own display as outside the update path. Older tests checked host runtime state but never the rendered title. Cached remote labels, unscoped identities, fire-and-forget renames, and stale replies introduced additional ways to display or persist the wrong value.", + "textKo": "호스트 렌더러의 syncAdoptedLabel 경로가 LOCAL_MACHINE_ID 변경 알림을 무시해 실제 호스트 화면이 갱신 경로에서 빠졌습니다. 기존 테스트는 호스트의 내부 상태만 검사하고 렌더된 제목을 검증하지 않았습니다. 오래된 캐시, 범위 없는 세션 ID, 응답 없는 변경 요청과 지연 응답도 불일치를 일으킬 수 있었습니다." + }, + "changes": [ + { + "titleEn": "Single host-owned write authority", + "titleKo": "호스트가 유일한 쓰기 권한을 보유", + "bodyEn": "Validate permissions, machine/project/provider identity, terminal incarnation and expected title revision. Reject stale writers with the owner's current snapshot.", + "bodyKo": "권한, 머신·프로젝트·공급자 ID, 터미널 인스턴스, 예상 제목 버전을 검증합니다. 오래된 수정 요청에는 호스트의 최신 스냅샷을 돌려줍니다." + }, + { + "titleEn": "Persist, then publish and acknowledge", + "titleKo": "저장 성공 후에만 화면 갱신·응답", + "bodyEn": "The host writes the disk record before updating runtime metadata, announcing the change or sending success. A disk failure leaves the previous committed title intact.", + "bodyKo": "호스트는 디스크 저장이 성공한 뒤 런타임 상태를 갱신하고 변경을 알리며 성공을 응답합니다. 저장 실패 때는 기존 제목을 유지합니다." + }, + { + "titleEn": "Keep the user's draft during conflicts", + "titleKo": "충돌 시 사용자의 편집 초안 보존", + "bodyEn": "A competing rename does not silently overwrite an open draft. Enter/Save explicitly commits; Escape discards. Delayed replies and obsolete cached titles cannot roll back newer state.", + "bodyKo": "다른 장치의 변경이 진행 중인 초안을 몰래 덮어쓰지 않습니다. Enter/저장만 확정하며 Escape는 취소합니다. 지연된 응답이나 오래된 캐시로 최신 상태를 되돌리지 못하게 합니다." + } + ], + "verification": { + "titleEn": "Verification beyond an API success response", + "titleKo": "API 응답을 넘어 실제 화면과 저장 상태 검증", + "focusedTests": 20, + "electronJourneys": 8, + "environmentEn": "Main Windows PC, three isolated Electron processes, real loopback TLS, synthetic terminal CLI, separate temporary profiles.", + "environmentKo": "MAIN Windows PC 한 대, 독립 Electron 프로세스 3개, 실제 루프백 TLS, 가짜 터미널 CLI와 별도의 임시 프로필.", + "commands": [ + "npm test -- src/main/sessionLabels.test.ts src/shared/sessionConsistency.test.ts", + "npm run qa:session-sync" + ], + "journeys": [ + { + "en": "Viewer rename updates host and both viewers", + "ko": "뷰어의 변경이 호스트·다른 뷰어에 반영" + }, + { + "en": "Host rename keeps the terminal mounted", + "ko": "호스트 이름 변경 시 터미널 재생성 방지" + }, + { + "en": "Stale edits are rejected without losing drafts", + "ko": "오래된 수정 차단·사용자 초안 보존" + }, + { + "en": "Disk failure avoids false success; retry works", + "ko": "디스크 오류 시 성공 오인 방지·재시도" + }, + { + "en": "Permission removal blocks unauthorized change", + "ko": "권한 제거 시 무단 변경 차단" + }, + { + "en": "Disconnect preserves draft; reconnect and retry", + "ko": "연결 끊김에도 초안 유지·재연결" + }, + { + "en": "Explicit clear survives viewer restart", + "ko": "제목 초기화가 뷰어 재시작 후에도 유지" + }, + { + "en": "Repeated rename cycles keep all three views aligned", + "ko": "반복 수정에도 세 화면이 일치" + } + ], + "limitationsEn": "Not two physical machines. Not long-running VPN/sleep/resume or live agent-provider acceptance. Real installer and updater journeys, human visual review and 1.37.8 public-release approval remain open. The baseline screenshot is historical, while the passing checks were rerun on 2026-10-12.", + "limitationsKo": "물리적으로 분리된 두 PC, 장기 VPN·절전 복구, 실제 AI 공급자 작업 부하는 검증하지 않았습니다. 설치 마법사·업데이터·사람의 시각 검수와 1.37.8 정식 출시 승인도 별도입니다. 실패 이미지는 과거 기록이고 통과 검사는 2026-10-12 재실행했습니다." + }, + "sources": [ + { + "kind": "baseline", + "labelEn": "Baseline commit", + "labelKo": "실패 기준 커밋", + "href": "https://github.com/writingdeveloper/devdeck/commit/b7817547dd2100751ce46229a83810319affe82b" + }, + { + "kind": "fix", + "labelEn": "Fix commit and diff", + "labelKo": "수정 커밋·변경 코드", + "href": "https://github.com/writingdeveloper/devdeck/commit/fa8864833b98e7d81f140e598b1f56d4d2b60c39" + }, + { + "kind": "review", + "labelEn": "Incident review", + "labelKo": "장애 분석·검증 기록", + "href": "https://github.com/writingdeveloper/devdeck/blob/fa8864833b98e7d81f140e598b1f56d4d2b60c39/docs/session-sync-review-2026-09-08.md" + }, + { + "kind": "unit", + "labelEn": "Focused unit tests", + "labelKo": "단위 회귀 테스트", + "href": "https://github.com/writingdeveloper/devdeck/blob/fa8864833b98e7d81f140e598b1f56d4d2b60c39/src/main/sessionLabels.test.ts" + }, + { + "kind": "harness", + "labelEn": "Three-app QA harness", + "labelKo": "Electron 3개 앱 QA 코드", + "href": "https://github.com/writingdeveloper/devdeck/blob/fa8864833b98e7d81f140e598b1f56d4d2b60c39/qa/session-sync.mjs" + }, + { + "kind": "ci", + "labelEn": "Same-commit green Windows CI", + "labelKo": "동일 커밋 Windows CI 통과", + "href": "https://github.com/writingdeveloper/devdeck/actions/runs/34319241204" + } + ] + } + ] +} diff --git a/content/posts/en/agent-engineering-ownership/index.mdx b/content/posts/en/agent-engineering-ownership/index.mdx new file mode 100644 index 0000000..83a311c --- /dev/null +++ b/content/posts/en/agent-engineering-ownership/index.mdx @@ -0,0 +1,96 @@ +--- +title: "How I Use Coding Agents Without Handing Them Ownership" +excerpt: "My workflow changed from AI-assisted coding to parallel agent execution. The important part is not how much code the agents write, but how architecture, evidence, gates, and release decisions stay explicit." +publishedAt: "2026-09-22" +author: "이시형" +category: development +tags: ["ai", "agents", "developer-tools", "evals", "mcp"] +project: devdeck +--- + +I no longer use coding AI mainly as autocomplete. Most of the time I give an agent a bounded job, let it inspect the repository and execute across files, and review the result as a system change. + +That sounds like delegating engineering. The distinction I care about is **what gets delegated**. + +Implementation can be delegated. Ownership cannot. + +## The unit of work got bigger + +A year ago, the useful unit was often a function or a file. Now I routinely ask Claude Code or Codex to investigate a failure, trace it across several layers, change the implementation, add regression coverage, and run the relevant gates. + +That changes the engineering bottleneck. Typing is less important. The difficult parts become: + +- defining the boundary of the task, +- making the source of truth explicit, +- deciding what evidence counts as done, +- separating model mistakes from system mistakes, +- and preventing a plausible-looking result from becoming a release. + +My recent projects are increasingly built around those boundaries. + +## DevDeck: parallel agents need state ownership + +[DevDeck](/en/projects/devdeck) started as a way to stop losing track of coding sessions across repositories. It grew into a local-first control plane for Claude Code, Codex, and Antigravity sessions. + +The interesting problem was not rendering more terminals. It was deciding who owns shared state. + +A session title edited from one machine cannot become true merely because one renderer displayed it. The host must own the write, persist it, acknowledge it, and make every observer converge on the same result. Failure, reconnect, stale responses, and restart all have to preserve that rule. + +The 1.37.7 review recorded **1,257 passing checks and one skip**. The dated 2026-09-08 maintenance candidate later recorded **1,289 passing checks and one skip**. These are version-scoped historical records, not a live test total. Separate source and packaged-application gates cover the later synchronization work; automated checks are not human or multi-day physical-network acceptance. + +The number matters less than what the tests are protecting: ownership rules around agent work. + +## studios: generation is not verification + +My [studios](/en/studio) platform exposes image, video, 3D, voice, and music generation through MCP tools. + +I deliberately split the pipeline into different responsibilities: + +1. the tool generates, +2. a machine gate rejects obvious failures, +3. the calling agent inspects the returned artifact, +4. the verdict and reason are persisted with the asset. + +The generator does not get to declare its own output good. + +That pattern now feels more important than any particular model choice. Models will change. A pipeline that makes quality boundaries inspectable survives the model swap. + +## Creator Learning OS: unfinished means blocked + +Creator Learning OS is moving toward a commercial SaaS that turns channel data into one concrete next action. + +Its release logic is intentionally conservative. Customer OAuth, billing, provider readiness, managed persistence, backup and recovery are not marketing checkboxes. If a production dependency is missing, release readiness should fail closed. + +I would rather have the system say **not ready** than let an agent reinterpret an incomplete check as approval. + +The same principle applies to AI features: an eval is useful only when failure has a consequence. + +## Worktrees before multi-agent orchestration + +When several agents work in parallel, I prefer isolated git worktrees over having them share one mutable checkout. + +The rule is simple: + +- one task, one worktree, +- explicit acceptance criteria, +- task-specific tests, +- inspect the diff, +- integrate only after the gate passes. + +This is not sophisticated multi-agent research. It is mostly ordinary software isolation applied to agents. That is exactly why it works. + +## What I still keep human-owned + +I use agents heavily, but there are boundaries I do not want to blur: + +- architecture and irreversible product decisions, +- what evidence is sufficient, +- security and privacy tradeoffs, +- whether a failed gate can be waived, +- and the final release decision. + +The goal is not to maximize the percentage of code written by AI. + +The goal is to increase the amount of useful work I can safely supervise while making the system easier to inspect when something goes wrong. + +That is the direction my current work is converging on: **agent systems, tool use, evals, state, observability, and production reliability around the model**. diff --git a/content/posts/ko/agent-engineering-ownership/index.mdx b/content/posts/ko/agent-engineering-ownership/index.mdx new file mode 100644 index 0000000..73d2e31 --- /dev/null +++ b/content/posts/ko/agent-engineering-ownership/index.mdx @@ -0,0 +1,96 @@ +--- +title: "코딩 에이전트에게 구현은 맡겨도 소유권은 넘기지 않는 방법" +excerpt: "AI 보조 코딩에서 병렬 에이전트 실행으로 작업 방식이 바뀌었습니다. 중요한 것은 AI가 코드를 얼마나 많이 쓰느냐가 아니라 아키텍처, 증거, 게이트, 릴리스 결정이 명시적으로 남는가입니다." +publishedAt: "2026-09-22" +author: "이시형" +category: development +tags: ["ai", "agents", "developer-tools", "evals", "mcp"] +project: devdeck +--- + +이제 코딩 AI를 주로 자동완성처럼 쓰지는 않습니다. 범위가 정해진 작업을 에이전트에게 주고, 저장소를 직접 조사하게 한 뒤 여러 파일에 걸쳐 구현하고 검증하게 하는 경우가 더 많습니다. + +겉으로 보면 엔지니어링 자체를 위임하는 것처럼 보입니다. 제가 중요하게 보는 구분은 **무엇을 위임하느냐**입니다. + +구현은 위임할 수 있습니다. 소유권은 위임하지 않습니다. + +## 작업의 단위가 커졌습니다 + +예전에는 함수나 파일 하나가 AI에게 맡기기 좋은 단위였다면, 지금은 Claude Code나 Codex에게 오류 원인을 추적하고, 여러 레이어를 수정하고, 회귀 테스트를 추가하고, 필요한 게이트까지 실행하게 하는 일이 흔합니다. + +그러면 병목도 달라집니다. 타이핑보다 아래 문제가 더 중요해집니다. + +- 작업 경계를 어디까지 둘지, +- source of truth가 무엇인지, +- 어떤 증거를 완료로 인정할지, +- 모델 오류와 시스템 오류를 어떻게 분리할지, +- 그럴듯한 결과가 바로 릴리스가 되지 않게 어떻게 막을지. + +최근 프로젝트들은 점점 이 경계를 중심으로 설계하고 있습니다. + +## DevDeck: 병렬 에이전트에는 상태 소유권이 필요합니다 + +[DevDeck](/projects/devdeck)은 여러 저장소의 코딩 세션을 놓치지 않기 위한 도구로 시작했고, 지금은 Claude Code·Codex·Antigravity 세션을 다루는 local-first control plane에 가깝습니다. + +어려운 문제는 터미널을 더 많이 그리는 것이 아니었습니다. 공유 상태를 누가 소유하는지 정하는 일이었습니다. + +한 머신에서 세션 제목을 바꿨다고 그 renderer가 보여주는 값이 곧 진실이 되어서는 안 됩니다. host가 쓰기를 소유하고, 디스크에 저장하고, 성공을 응답하고, 다른 observer들이 같은 결과로 수렴해야 합니다. 실패·재연결·stale response·재시작이 있어도 이 규칙이 유지돼야 합니다. + +1.37.7 검토에는 **1,257개 통과·1개 생략**, 2026-09-08 유지보수 후보 기록에는 **1,289개 통과·1개 생략**이 있습니다. 서로 다른 버전의 과거 검증 기록이며 현재 실행한 테스트 수가 아닙니다. 이후 동기화 작업은 소스·패키지 게이트를 별도로 유지하고, 자동 검사를 사람의 시각 검수나 장기간 물리 네트워크 검증과 동일시하지 않습니다. + +숫자 자체보다 중요한 것은 그 테스트가 에이전트 작업의 **소유권 규칙**을 지키고 있다는 점입니다. + +## studios: 생성과 검수는 같은 책임이 아닙니다 + +[studios](/studio)는 이미지·영상·3D·음성·음악 생성을 MCP 툴로 노출합니다. + +파이프라인의 책임을 일부러 나눴습니다. + +1. 툴이 생성하고, +2. 기계 게이트가 명백한 실패를 제거하고, +3. 호출한 에이전트가 결과물을 직접 보고, +4. 판정과 이유를 자산과 함께 저장합니다. + +생성기가 자기 결과를 스스로 합격시키지 못하게 한 것입니다. + +이 구조는 특정 모델보다 오래 갈 수 있다고 봅니다. 모델은 바뀌어도 품질 경계가 관찰 가능하게 남는 파이프라인은 유지할 수 있습니다. + +## Creator Learning OS: 미완료는 승인으로 해석하지 않습니다 + +Creator Learning OS는 채널 데이터를 한 가지 구체적인 다음 행동으로 바꾸는 상용 SaaS 방향으로 개발하고 있습니다. + +릴리스 판정은 의도적으로 보수적으로 만들고 있습니다. 고객 OAuth, 결제, provider readiness, 영속 DB, 백업·복구는 마케팅 체크박스가 아닙니다. 프로덕션 의존성이 빠져 있으면 readiness가 fail-closed 해야 합니다. + +에이전트가 불완전한 검사를 승인으로 재해석하는 것보다 시스템이 **아직 준비되지 않음**이라고 말하는 편이 낫습니다. + +AI 기능의 eval도 마찬가지입니다. 실패했을 때 실제로 막히는 것이 있어야 평가가 의미를 가집니다. + +## 멀티 에이전트보다 먼저 git worktree + +여러 에이전트를 병렬로 돌릴 때는 하나의 checkout을 공유시키기보다 격리된 git worktree를 선호합니다. + +규칙은 단순합니다. + +- 작업 하나당 worktree 하나, +- 명시적인 acceptance criteria, +- 작업별 테스트, +- diff 검수, +- gate 통과 후에만 통합. + +복잡한 multi-agent 연구라기보다 기존 소프트웨어 격리 원칙을 에이전트에 적용한 것입니다. 그래서 오히려 안정적으로 작동합니다. + +## 여전히 사람이 소유하는 것 + +에이전트를 많이 사용하지만 아래 경계는 흐리지 않으려고 합니다. + +- 아키텍처와 되돌리기 어려운 제품 결정, +- 어떤 증거면 충분한지, +- 보안·프라이버시 trade-off, +- 실패한 게이트를 예외 처리할 수 있는지, +- 최종 릴리스 결정. + +목표는 AI가 작성한 코드 비율을 최대화하는 것이 아닙니다. + +문제가 생겼을 때 더 쉽게 추적할 수 있는 상태를 유지하면서, 한 사람이 안전하게 감독할 수 있는 유용한 작업량을 늘리는 것이 목표입니다. + +현재 제 작업은 점점 **모델 주변의 agent systems, tool use, evals, state, observability, production reliability** 쪽으로 모이고 있습니다. diff --git a/content/projects.json b/content/projects.json index df445a3..a80fdcb 100644 --- a/content/projects.json +++ b/content/projects.json @@ -1,10 +1,10 @@ { "projects": [ { - "name": "Soursea", + "name": "Soursea — Personal Sourcing Workspace", "slug": "soursea", - "descriptionKo": "한국→미국 셀러용 Electron 소싱 데스크톱 도구. 내장 브라우저에서 공급처 페이지를 구조화된 로컬 상품 데이터로 추출하고, 운영 보드와 MCP를 통해 Claude Code 분석까지 연결합니다.", - "descriptionEn": "Electron desktop sourcing workspace for a Korea→US seller. An embedded browser turns supplier pages into structured local product records, then connects that data to an operations board and Claude Code through MCP.", + "descriptionKo": "Soursea LLC의 고객용 서비스 목록과 별개인 개인용 Electron 상품 소싱 도구입니다. 내장 브라우저에서 공급처 정보를 정규화해 로컬 SQLite에 저장하고 MCP로 분석 워크플로에 연결하지만, 공개 SaaS로 제공하지 않습니다.", + "descriptionEn": "A personal-use Electron sourcing workspace (not the Soursea LLC customer product catalog). An embedded browser normalizes supplier pages into local SQLite records and connects operations data to a local MCP client; it is not offered as a public SaaS.", "techStack": [ "Electron", "TypeScript", @@ -34,10 +34,10 @@ } }, { - "name": "Creator OS", + "name": "Creator OS — Release Engineering", "slug": "creator-os", - "descriptionKo": "크리에이터·YouTube 리포팅 SaaS를 실제 고객 환경에 내보내기 전에 실패 폐쇄형 릴리스 게이트로 단단하게 만든 production-hardening 프로젝트. PostgreSQL 워커, Docker 런타임, 백업·복구, Playwright QA와 운영 프리플라이트를 한 릴리스 계약으로 묶었습니다.", - "descriptionEn": "A production-hardening project for a creator and YouTube reporting SaaS. Before exposing customer traffic, I tied PostgreSQL workers, Docker runtime, backup/recovery, Playwright QA, and fail-closed operator preflight into one release contract.", + "descriptionKo": "별도 SaaS가 아니라 같은 Creator OS 제품의 출시 엔지니어링 사례입니다. PostgreSQL 워커·Docker 배포 정체성, 암호화 백업·복구 리허설, 운영자 사전 점검과 읽기 전용 파일럿 검증을 통해 빌드 성공과 실제 고객용 출시를 구분합니다.", + "descriptionEn": "An engineering case study of the same Creator OS product, not another standalone SaaS. It separates a working build from customer-safe deployment using PostgreSQL worker checks, Docker deployment identity, encrypted backups, restore rehearsals, explicit operator preflights, and read-only pilot verification.", "techStack": [ "Next.js", "TypeScript", @@ -51,7 +51,7 @@ "status": "active", "private": true, "featured": false, - "updatedAt": "2026-09-23", + "updatedAt": "2026-10-08", "screenshot": "/images/projects/creator-os.png", "screenshotAltKo": "Creator OS 비공개 스테이징의 실제 Today 리포트 화면. 배포 전 QA에서 캡처한 제품 UI.", "screenshotAltEn": "The real Creator OS Today report UI captured during private-staging QA before public launch.", @@ -62,15 +62,15 @@ "decisionEn": "I separated source completion from launch approval. The release uses immutable manifests, a dedicated PostgreSQL worker, Docker/Compose verification, backup-and-restore plus an independent deletion ledger, fail-closed operator/preflight checks, and a separate FINAL-QA gate. Missing external evidence stays blocked instead of being treated as success.", "evidenceKo": "최근 COS-32/33 최종 QA에서 focused 22/22, 전체 회귀 669건 통과(27건은 DB 미구성으로 skip이며 pass로 세지 않음), typecheck·production build·release verifier가 통과했습니다. 올바른 4080 private-staging host에 대한 29/29 read-only live checks도 통과했습니다.", "evidenceEn": "The latest COS-32/33 final QA passed 22/22 focused checks and 669 regression tests; 27 database-dependent tests were skipped and explicitly not counted as passes. Typecheck, production build, release verifiers, and 29/29 read-only checks against the intended 4080 private-staging host also passed.", - "resultKo": "반복 가능한 private-staging 릴리스 증거와 운영 경계를 만들었지만, public launch는 DNS 쓰기 인증·공개 TLS/proxy·실제 Google 동의·정책/지원 정보·이메일 sender·암호화 백업 키 보관 등 외부 게이트가 남아 있어 의도적으로 차단 상태로 유지했습니다.", - "resultEn": "The result is a reproducible private-staging release with explicit operational evidence. Public launch remains intentionally blocked on external gates such as authoritative DNS access, public TLS/proxy verification, real Google consent, policy/support approval, email sender identity, and encrypted recovery-key custody." + "resultKo": "10월 6일부터 승인된 Google 테스트 사용자와 읽기 전용 YouTube 동의를 사용하는 제한된 외부 파일럿을 운영했습니다. 누구나 가입하는 공개 출시·결제는 별도이며, 이 항목은 두 번째 제품이 아닌 출시 안전성 엔지니어링 기록입니다.", + "resultEn": "A bounded external pilot was activated on October 6 for approved Google test users, with real read-only YouTube consent. Broad public registration and billing are not available; this entry tracks engineering and release safety, not a second product." } }, { "name": "citefirst", "slug": "citefirst", - "descriptionKo": "문서 기반 질의응답(RAG)에서 인용을 화면에 내보내기 전에 검색된 청크와 대조해 검증하고, 검색 품질을 주장이 아니라 측정으로 답하는 시스템. pg_dump와 pg_dumpall, 다섯 종류의 work_mem 설정처럼 거의 똑같아 보이는 문단이 가득한 PostgreSQL 17 공식 매뉴얼 약 3,000쪽을 코퍼스로 골라, 순진한 벡터 검색이 조용히 엉뚱한 문단을 돌려주는 지점을 드러냈습니다. 46개 질문 평가에서 크로스 인코더 재순위화를 켜면 recall@5 0.978, MRR@10 0.834로 평문 벡터 검색의 0.652를 크게 웃돌고, 인용 유효성은 1.000으로 168건 중 환각 인용이 0건입니다. 임베딩과 재순위화가 로컬 ONNX로 돌아 색인 단계에는 외부 호출이 전혀 없습니다.", - "descriptionEn": "A RAG system that verifies every citation against the retrieved chunks before it ever reaches the screen, and answers the question of retrieval quality by measuring it rather than asserting it. The corpus is ~3,000 pages of the PostgreSQL 17 manual — full of passages that look nearly identical (pg_dump vs pg_dumpall, five different work_mem settings), exactly where naive vector search quietly returns the wrong paragraph. Across 46 evaluation questions, cross-encoder reranking scores recall@5 0.978 and MRR@10 0.834 against 0.652 for plain vector search, with citation validity at 1.000 and zero hallucinated citations out of 168. Embeddings and reranking run locally through ONNX, so ingestion makes no outbound calls at all.", + "descriptionKo": "PostgreSQL 17 매뉴얼을 대상으로 로컬 임베딩·재순위화, 질의 재작성, 인용 ID 검증을 결합한 RAG 시스템입니다. 커밋된 46개 질문 구성에서 recall@5 0.978·MRR@10 0.834를 기록했습니다. 인용 ID 포함 여부·모델이 판정한 근거성·답변 정확도는 별도 평가이며, 이후 56문항 로컬 실험은 공개 기준값이 아닌 검토 후보로 남겨 두었습니다.", + "descriptionEn": "A RAG system over the PostgreSQL 17 manual with locally executed embeddings/reranking, query rewriting and citation-ID validation. The committed 46-question configuration records recall@5 0.978 and MRR@10 0.834. Citation membership, model-judged grounding and answer correctness are evaluated separately. A newer 56-question local experiment remains a review candidate, not a published baseline.", "techStack": [ "TypeScript", "Next.js", @@ -94,8 +94,8 @@ "problemEn": "A RAG answer having citations does not prove that the citations support it or that retrieval found the right passage.", "decisionKo": "PostgreSQL 17 매뉴얼처럼 유사 문단이 많은 코퍼스를 평가셋으로 삼고 로컬 임베딩·크로스 인코더 재순위화·출력 전 인용 검증을 분리해 측정했습니다.", "decisionEn": "I used the PostgreSQL 17 manual as a deliberately confusable corpus, then measured local embeddings, cross-encoder reranking, and pre-output citation validation separately.", - "evidenceKo": "46개 평가 질문에서 재순위화 구성은 recall@5 0.978, MRR@10 0.834를 기록했고 인용 유효성은 1.000으로 168건 중 환각 인용이 0건이었습니다.", - "evidenceEn": "Across 46 evaluation questions, reranking reached recall@5 0.978 and MRR@10 0.834; citation validity was 1.000 with zero hallucinated citations in 168.", + "evidenceKo": "고정된 7월 소스에서 PostgreSQL 17 질문 46개, 재순위화 구성 recall@5 0.978·MRR@10 0.834를 기록했습니다. 인용 유효성은 검색 청크에 ID가 있는지 확인하는 것으로 답변의 진실성 보장이 아닙니다. 56문항 작업본 결과는 미커밋 상태이므로 공개 기준값을 자동 대체하지 않습니다.", + "evidenceEn": "Pinned July source: 46 PostgreSQL 17 questions, reranking-on recall@5 0.978 and MRR@10 0.834. Citation validity checks membership in retrieved chunks, not truth of the answer. The 56-question worktree experiment has not been committed and must not silently replace this published reference.", "resultKo": "검색 품질과 인용 정확도를 반복 가능한 수치로 비교할 수 있는 RAG 평가·검증 시스템이 됐습니다.", "resultEn": "Retrieval quality and citation correctness became repeatable measurements instead of qualitative claims." } @@ -117,7 +117,7 @@ "status": "active", "github": "https://github.com/writingdeveloper/JuiceBar", "private": false, - "featured": true, + "featured": false, "updatedAt": "2026-08-26", "screenshot": "/images/projects/juicebar.webp", "screenshotAltKo": "JuiceBar 트레이 팝업 실제 화면. 연료 게이지 형태의 진행 표시와 이번 청구 주기 비용, 오늘 사용량, 예상 비용, 최근 추세를 보여준다.", @@ -136,8 +136,8 @@ { "name": "DevDeck", "slug": "devdeck", - "descriptionKo": "여러 프로젝트를 Claude Code·Codex·Antigravity로 굴리는 개발자를 위한 Electron 관제 데스크톱 앱. 설정한 폴더의 git 레포를 스캔해 git 상태와 방치 기간, 에이전트별 세션 이력을 카드로 보여주고 클릭 한 번으로 마지막 세션을 이어 갑니다. Windows에서는 외부 터미널이 우르르 뜨는 대신 앱 안에 터미널을 여는 조종석을 제공해, 세션마다 모델과 작업 시간, 한계에 가까워질수록 색이 짙어지는 컨텍스트 퍼센트, 그리고 그 세션이 지금 무엇을 하고 있는지 한 줄 요약을 함께 보여주고 급한 순서로 묶어 정렬합니다. 요약을 AI로 다듬을 때는 그 세션을 연 에이전트의 CLI가 직접 처리해 한쪽 벤더의 사용량이 다른 쪽 작업에 쓰이지 않게 했습니다. contextIsolation·sandbox·argv 인젝션 방어 등 Electron 보안을 1급 관심사로 설계하고 465개 테스트와 Playwright+axe e2e 게이트로 검증했습니다.", - "descriptionEn": "An Electron command deck for developers juggling a pile of Claude Code, Codex and Antigravity projects. It scans git repos under configured folders and surfaces git state, how long each has been neglected, and per-agent session history as cards, resuming your last session in one click. On Windows a Cockpit replaces the wall of external terminals with in-app ones: each session shows its model, active working time, a context percentage that colors up as it nears the limit, and a one-line summary of what it is actually working on — grouped by urgency, so you can see who is waiting on you. When AI refinement is on, each summary is condensed by that session's own agent CLI, so one vendor's quota is never spent on another's work. I treated Electron security as a first-class concern (contextIsolation, sandbox, argv-not-string-interpolation against command injection) and backed it with 465 tests and a Playwright + axe e2e gate.", + "descriptionKo": "여러 저장소의 Claude Code·Codex·Antigravity 작업을 모아 보는 local-first Electron 관제 앱입니다. 저장소·세션 상태, 내장 터미널, usage/context 신호를 연결합니다. 문서화된 1.37.8 후보 버전에서는 host-owned 메타데이터·영속 저장·명시적 충돌 복구를 다룹니다. 자동 검증과 실제 출시·설치·물리 네트워크 검수를 구분해 기록합니다.", + "descriptionEn": "A local-first Electron command center for Claude Code, Codex and Antigravity work across repositories. It brings repository/session state, embedded terminals and usage/context signals together. A documented 1.37.8 candidate adds host-owned metadata, persisted writes and explicit conflict recovery. The engineering record separates automated checks from release, installer and physical-network acceptance.", "techStack": [ "TypeScript", "Electron", @@ -151,7 +151,7 @@ "github": "https://github.com/writingdeveloper/devdeck", "private": false, "featured": true, - "updatedAt": "2026-08-26", + "updatedAt": "2026-09-08", "screenshot": "/images/projects/devdeck.webp", "screenshotAltKo": "DevDeck 프로젝트 관제 화면. 여러 Git 프로젝트와 상태, 작업 재개 진입점을 한 화면에 모은 실제 Electron UI.", "screenshotAltEn": "The real DevDeck Electron project command center, listing multiple Git projects with status and resume entry points.", @@ -160,10 +160,10 @@ "problemEn": "When many projects run across different coding agents, a wall of terminals makes it hard to see which session stalled and which needs a human decision.", "decisionKo": "Git 상태·방치 시간·세션 이력을 모으는 Electron 관제판과 앱 내부 Cockpit을 만들고 요약은 각 세션을 연 에이전트 CLI가 담당하도록 분리했습니다.", "decisionEn": "I built an Electron command deck over Git state, neglect time, and session history, plus an in-app Cockpit; each session is summarized by the agent CLI that owns it.", - "evidenceKo": "공개 저장소에는 프로젝트·Cockpit·사용량 화면의 실제 QA 스크린샷과 Electron 보안 및 Playwright/axe 검증 흐름이 함께 유지됩니다.", - "evidenceEn": "The public repository keeps real QA screenshots for projects, Cockpit, and usage alongside Electron-security and Playwright/axe verification paths.", - "resultKo": "여러 에이전트 세션을 작업 우선순위와 재개 지점 중심으로 보는 단일 데스크톱 관제면을 만들었습니다.", - "resultEn": "The result is one desktop control surface organized around agent-session priority and resumability rather than terminal windows." + "evidenceKo": "2026-09-08 유지보수 기록은 1.37.8 후보에서 단위 검사 1,289개 통과·1개 생략과 maintenance/resilience 시나리오를 보고합니다. 이후 동기화 작업은 별도 게이트가 있습니다. 오늘의 실행 결과나 공개 출시 증거가 아닌, 버전과 범위를 한정한 기록입니다.", + "evidenceEn": "The 2026-09-08 maintenance record reports 1,289 passed and one skipped unit check for the 1.37.8 candidate, plus maintenance and resilience journeys. Later synchronization work has its own gate. These are historical, version-scoped records, not today's test count or proof of public release.", + "resultKo": "여러 머신·실패·재연결·재시작에서도 소유권과 동기화 규칙이 유지되는, 병렬 코딩 에이전트 작업용 검증 가능한 control plane을 만들었습니다.", + "resultEn": "The result is an inspectable control plane for parallel coding-agent work, with ownership and synchronization rules that survive multiple machines, failures, reconnects and restarts." } }, { @@ -535,6 +535,114 @@ "private": true, "featured": true }, + { + "name": "Creator OS — Product & External Pilot", + "slug": "creator-learning-os", + "descriptionKo": "“지금 무엇을 개선해야 하나?”에 답하도록 설계한 크리에이터·YouTube 의사결정 SaaS입니다. 10월 6일부터 공개 호스트의 외부 파일럿이 운영되지만 Google 로그인은 승인된 테스트 사용자에게만 제공됩니다. 별도 읽기 전용 YouTube 동의, 근거 기반 결정론적 리포트, 비로그인 데모, 백업·복구 및 운영 게이트가 검증됐습니다. 일반 회원가입·유료 출시·실시간 LLM 추론 서비스는 아닙니다.", + "descriptionEn": "A creator and YouTube decision-support SaaS for answering “what should I improve now?” October 6 brought an external pilot on a public host, but Google sign-in remains Testing-only for approved users. Separate read-only YouTube consent, evidence-bound deterministic reports, a public no-login demo, backup/restore drills, and operator release gates are verified. This is not unrestricted signup, paid availability, or live LLM inference.", + "techStack": [ + "TypeScript", + "Next.js", + "PostgreSQL", + "SQLite", + "Playwright", + "Vitest", + "YouTube Data API" + ], + "status": "building", + "private": true, + "featured": false, + "updatedAt": "2026-10-08", + "caseStudy": { + "problemKo": "크리에이터 분석 도구는 지표를 많이 보여줘도 사용자가 다음에 무엇을 해야 하는지 다시 해석해야 하는 경우가 많습니다.", + "problemEn": "Creator analytics often expose more metrics while still making the user decide what to do next.", + "decisionKo": "대시보드의 중심을 점수나 차트가 아니라 '지금 할 한 가지'로 두고, 근거와 진단은 필요할 때만 펼치도록 설계했습니다. 릴리스 준비도는 미완료 의존성이 있으면 fail-closed 처리합니다.", + "decisionEn": "I centered the product on one concrete next action rather than an opaque score, with evidence and diagnostics available on demand. Release readiness fails closed when production dependencies remain incomplete.", + "evidenceKo": "10월 6일 공개 호스트 전환 기록에서 Cloudflare TLS·AOP 신뢰 경계와 승인된 테스트 사용자 Google 로그인, 별도 읽기 전용 YouTube 동의를 확인했습니다. 10월 8일에는 한영 약관·지원 화면, 모델 API 호출 없는 근거 중심 결정론적 리포트, 외부 저장소 PostgreSQL 복구 리허설을 검증했습니다. /demo는 비로그인 접근 가능하지만 불특정 사용자 가입이나 실사용자 규모는 확인하지 않았습니다.", + "evidenceEn": "An October 6 public-host activation verified Cloudflare TLS, AOP trust boundary and allowed-test-user Google login with separate YouTube read-only consent. The October 8 release record verified bilingual policy/support pages, deterministic grounded reports without a provider model call, and an off-device PostgreSQL backup restore drill. Public /demo is accessible without login; production external-user volume and unrestricted signup were not verified.", + "resultKo": "비로그인 데모와 승인된 Google 테스트 사용자 로그인을 갖춘 외부 파일럿 단계입니다. 일반 고객 확대, 결제, 모델 API 추론 및 무제한 셀프서비스는 별도 출시 과제입니다.", + "resultEn": "An external pilot with a public read-only demo and a restricted Google test-user login is running. Customer-scale rollout, billing, provider-model inference and unrestricted self-service remain separate unapproved releases." + }, + "website": "https://creator.writingdeveloper.blog/demo" + }, + { + "name": "RailForge", + "slug": "railforge", + "descriptionKo": "철도 건설·운영·운전을 하나의 3D 웹 세계에서 연결하려는 브라우저 게임 프로토타입입니다. 선로 곡선·대차 움직임·연동장치·공유 운전 권한을 독립 실험으로 검증하고 있습니다. 현재 개발·테스트 중인 연구 프로젝트이며 정식 멀티플레이 게임으로 출시하지 않았습니다.", + "descriptionEn": "A research-first 3D browser railway prototype combining network construction, dispatch, and direct train driving. Separate experiments probe spline/bogie motion, interlocking, multiplayer authority, and a playable two-town loop. The integrated multiplayer game is not a public release.", + "techStack": [ + "TypeScript", + "Three.js", + "Vite", + "Vitest" + ], + "status": "building", + "private": true, + "featured": false, + "updatedAt": "2026-09-29", + "caseStudy": { + "problemKo": "철도 시뮬레이션에서 네트워크 설계·운영·운전자 조작을 같은 세계의 일관된 상태로 연결하기 어렵습니다.", + "problemEn": "Railway games often separate network planning, dispatch operations, and interactive driving into disconnected experiences.", + "decisionKo": "하나의 거대한 게임을 먼저 만들기보다 선형·대차·분기기 연동·공유 권한의 핵심 위험을 작은 독립 실험으로 분해했습니다.", + "decisionEn": "Instead of declaring a full game complete, I split core risks into isolated experiments for rail geometry, bogies, interlocking, and shared driving authority.", + "evidenceKo": "4080 PC의 RailForge 저장소에 Three.js/Vite 기반 playable-toy, dispatcher, multiplayer 및 spline-bogie 실험 코드와 검사 스크립트가 존재합니다. 일부 작업은 미커밋이며 전체 플레이 경험 검증은 별도입니다.", + "evidenceEn": "The MAIN-PC repository contains a Three.js/Vite playable-toy and separate dispatcher, multiplayer, and spline-bogie experiments with verification scripts. Some work remains uncommitted; end-to-end player acceptance is a separate gate.", + "resultKo": "연구용 프로토타입과 기술 실험 단계입니다. 안정적인 정식 출시나 만족스러운 전체 게임플레이는 아직 입증하지 않았습니다.", + "resultEn": "Prototype and engineering research only. Stable public launch and accepted end-to-end gameplay have not been demonstrated." + } + }, + { + "name": "Project STRATA", + "slug": "project-strata", + "descriptionKo": "거대한 폐허 산업 도시를 탐험하며 시설을 복구하는 Unreal Engine 5.8 싱글플레이 게임 기술 스파이크입니다. 차량 MULE과 인프라 퍼즐의 핵심 감각을 검증하고 있으며 30~45분 버티컬 슬라이스 승인 전에는 정식 제작·출시로 주장하지 않습니다.", + "descriptionEn": "An Unreal Engine 5.8 technical spike for an atmospheric, vehicle-led exploration game set in a vast abandoned industrial city. MULE traversal and infrastructure-reactivation puzzles remain subject to a 30–45-minute vertical-slice acceptance gate; it is not a shipped game.", + "techStack": [ + "Unreal Engine 5.8", + "3D Gameplay", + "Prototyping" + ], + "status": "building", + "private": true, + "featured": false, + "updatedAt": "2026-10-06", + "caseStudy": { + "problemKo": "전투 없이 이동·탐색·시설 복구만으로 몰입도 있는 게임 흐름이 성립하는지 검증해야 했습니다.", + "problemEn": "The central question is whether traversal, discovery, and restoring dead infrastructure form a satisfying loop without combat.", + "decisionKo": "전면 제작보다 MULE 이동·환경 탐색·복구의 시각적 보상이 연결되는 버티컬 슬라이스를 먼저 설정했습니다.", + "decisionEn": "I set a vertical-slice gate for MULE traversal, environmental exploration, and visible world-state changes before committing to full production.", + "evidenceKo": "MAIN PC의 project-strata/game/Strata/Strata.uproject와 설계·QA 문서, 2026-10-06 커밋을 확인했습니다. 게임플레이 수용 QA는 미완료입니다.", + "evidenceEn": "The MAIN-PC repository contains a Strata.uproject, implementation and QA plans, and a 2026-10-06 committed technical-spike update. Gameplay acceptance remains open.", + "resultKo": "Unreal 기술 검증 단계로 플레이 가능성과 재미가 충분히 입증되지 않아 출시작으로 표시하지 않습니다.", + "resultEn": "An Unreal feasibility experiment, not a released product. Full gameplay and fun-factor acceptance remain open." + } + }, + { + "name": "Music / MV Studio", + "slug": "music-mv-studio", + "descriptionKo": "여러 로컬 생성 모델의 음악 결과물을 원본 이동 없이 프로젝트 단위로 정리하고 비교·선정하며 MV 컷을 계획하는 비공개 제작 도구입니다. Notebook의 WebUI/SQLite와 MAIN PC의 생성 작업을 연결하고 있지만 실제 H3 렌더 제출 연동은 아직 구현 중입니다.", + "descriptionEn": "A private project-first control surface for multi-model music and music-video creation. It catalogs immutable generator outputs across Notebook and MAIN, supports A/B listening and approval states, and plans MV shots. H3 render submission remains explicitly unwired.", + "techStack": [ + "Node.js", + "SQLite", + "Vite", + "Python", + "ComfyUI" + ], + "status": "building", + "private": true, + "featured": false, + "updatedAt": "2026-10-09", + "caseStudy": { + "problemKo": "음악 모델마다 산재한 생성 결과를 원본을 훼손하지 않고 비교·선정·영상화하기 어렵습니다.", + "problemEn": "Music-model outputs become scattered artifacts, making review, version selection, and MV planning hard without mutating source files.", + "decisionKo": "엔진 출력 폴더는 읽기 전용 이력으로 유지하고, 작업 상태·승인·A/B 비교·스토리보드와 작업 큐를 별도 SQLite 카탈로그로 관리합니다.", + "decisionEn": "I leave engine output folders immutable while a SQLite catalog manages project state, approvals, synchronized A/B review, MV shot windows, and queued work.", + "evidenceKo": "Notebook의 music-mv-studio README와 구현에 WebUI, SQLite 카탈로그, lazy 오디오 캐시, A/B 청취, 검증 스크립트가 있으며 H3 워크플로 바인딩 미완료가 명시되어 있습니다.", + "evidenceEn": "The Notebook music-mv-studio README and implementation document its WebUI, SQLite catalog, lazy audio cache, A/B listening, and QA scripts; H3 workflow binding is explicitly incomplete.", + "resultKo": "개인 연구·제작 과정에서 사용하는 비공개 도구입니다. 공개 음악 SaaS나 완성된 자동 MV 제작기로 오인하지 않도록 구분합니다.", + "resultEn": "A private creative-workflow tool under active development, not a public music SaaS or finished automated MV generator." + } + }, { "name": "til-shorts", "slug": "til-shorts", @@ -636,7 +744,7 @@ "website": "https://healframe.app", "playStore": "https://play.google.com/store/apps/details?id=app.healframe", "private": true, - "featured": false, + "featured": true, "screenshot": "/images/projects/healframe.webp", "screenshotAltKo": "HealFrame 랜딩 화면. 상단 내비게이션에 Circle·Dashboard·Pricing·Account가 있고, 'Reset your nervous system in 2 minutes' 헤드라인 아래 'Tap to breathe'라고 적힌 원형 호흡 버튼과 초록색 시작 버튼이 놓여 있다.", "screenshotAltEn": "HealFrame landing page. The top nav shows Circle, Dashboard, Pricing and Account; below the headline 'Reset your nervous system in 2 minutes' sit a gradient circle labelled 'Tap to breathe' and a green 'Start breathing — it's free' button." @@ -703,7 +811,7 @@ "website": "https://rentrights.writingdeveloper.blog", "github": "https://github.com/writingdeveloper/rentrights", "private": false, - "featured": true, + "featured": false, "screenshot": "/images/projects/rentrights.webp", "screenshotAltKo": "RentRights 랜딩 화면. 'You have rights.' 헤드라인 아래 로스앤젤레스 주소를 넣는 입력창과 Check 버튼이 있고, 공공기록·갱신일·주소 미저장 배지에 이어 3단계 이용 안내와 RSO 관련 FAQ가 보인다.", "screenshotAltEn": "RentRights landing page. Under the headline 'You have rights.' sit an address field showing a Los Angeles example and a Check button, followed by badges for public records, the update date and 'free, no address saved', a three-step how-it-works list and an FAQ about LA's Rent Stabilization Ordinance." @@ -957,6 +1065,40 @@ "screenshot": "/images/projects/tantrum-tower.webp", "screenshotAltKo": "Tantrum Tower 시작 화면. 어두운 카드 안에 노란 TANTRUM TOWER 로고와 한 줄 소개, '스토리 시작' 버튼과 쉬움·보통·어려움 난이도 선택, '현장 투입' 버튼이 있고, 그 아래 조작키 안내와 현장 이벤트 목록이 정리돼 있다.", "screenshotAltEn": "Tantrum Tower start screen. Inside a dark panel, the glowing yellow logo sits above a one-line Korean pitch, a story-start button, easy/normal/hard difficulty buttons and a quick-play button, with a key-binding list and a table of site events underneath." + }, + { + "name": "BRAIN//VAT", + "slug": "brain-vat-siege", + "descriptionKo": "Three.js 보스전과 신경 컨트롤러 연구를 하나의 공개 인터랙티브 케이스 스터디로 만든 프로젝트입니다. 미학습·사전학습·온라인 적응 예측기를 같은 입력에서 비교하고, 플레이로 달라진 102개 가중치 기억을 호환 보스 몸체로 옮길 수 있습니다. 별도 연구 경로에서는 166,700-뉴런 MaleCNS 고정 connectome의 시간 상태에 작은 외부 readout을 붙여 held-out 움직임 이력 분류를 검증했습니다. 10월 13일 공개된 별도 /npc 데모에서는 제한된 관측과 목격 이벤트 기억, 세계 상태에 따른 행동 검증을 갖춘 규칙 기반 경비 NPC를 구현했습니다. 학습 모델이나 LLM NPC는 아닙니다.", + "descriptionEn": "A public interactive case study combining a Three.js boss fight with neural-controller research. It compares untrained, pretrained-frozen and online-adaptive predictors on the same committed inputs, and lets a player move a 102-weight learned memory cartridge into a compatible boss chassis. A separate research path keeps the 166,700-neuron MaleCNS connectome fixed and tests small external readouts over its temporal state on a held-out motion-history task. On October 13, a separate public /npc demo added a deterministic rule-driven guard with filtered observations, witnessed-event memory, and world-validated actions. It is not an LLM or trained NPC.", + "techStack": [ + "JavaScript", + "Three.js", + "Node.js", + "Playwright", + "Python", + "C++", + "WebGL2", + "Vercel" + ], + "status": "active", + "website": "https://brain-vat-siege.vercel.app", + "private": true, + "featured": true, + "updatedAt": "2026-10-13", + "screenshot": "/images/projects/brain-vat-siege.webp", + "screenshotAltKo": "BRAIN//VAT SENTINEL 보스전의 실제 Three.js 화면. 중앙의 발광 기계형 보스가 세 개의 네온 레인을 향해 공격을 준비하고 있다.", + "screenshotAltEn": "A real Three.js capture of the BRAIN//VAT SENTINEL boss fight, with a glowing mechanical boss preparing an attack over three neon lanes.", + "caseStudy": { + "problemKo": "신경망이 동작한다는 그래프만으로는 사용자가 무엇을 할 수 있는지, 그리고 AI의 기억·예측이 실제 플레이를 어떻게 바꾸는지 체감하기 어려웠습니다.", + "problemEn": "Neural telemetry alone did not make it obvious what a player could do with the system or how prediction and memory changed an actual interaction.", + "decisionKo": "예측을 사용자 입력 전에 잠그는 3레인 보스전을 만들고 미학습·고정 사전학습·온라인 적응 모델을 같은 회피 기록으로 비교했습니다. 이어 플레이로 변한 102개 예측 가중치만 호환 보스 chassis로 옮기는 기억 카트리지와 게임 소유 action budget을 분리했습니다. 후속 NPC 실험에서는 이동·시야·세계 이벤트·허용된 의도·NPC 위치를 브라우저의 게임 엔진이 결정하게 하고, 규칙 컨트롤러는 제한된 상위 행동만 제안하도록 분리했습니다.", + "decisionEn": "I built a three-lane boss fight that commits predictions before player input, compares fresh, frozen-pretrained and online-adaptive models on the same dodge trace, and separates game-owned action budgets from a 102-weight predictor memory cartridge that can move only between compatible boss chassis. A later NPC experiment keeps player movement, sight, world events, permitted intent, and NPC motion inside the authoritative local-browser simulation; the controller only proposes bounded high-level actions.", + "evidenceKo": "공개 production은 `/boss`, `/demo`, `/evaluation`을 외부 AI API 없이 실행합니다. SENTINEL의 실제 Chrome QA는 합법적인 키 입력으로 10회 반격 승리·무조작 패배를 모두 재현했고, 기억 카트리지는 SENTINEL에서 WRAITH로 이동 후 HP·라운드는 초기화되면서 예측 가중치만 유지됐습니다. MaleCNS 별도 평가에서는 최종 프레임만 30/90(33.3%), 36개 calibrated optic cell의 시간 상태+111개 외부 readout 파라미터가 66/90(73.3%)였으며 connectome weight SHA는 전후 동일했습니다. 2026-10-13 BVAT-37의 공개 /npc 데모는 소스 테스트 200/200과 로컬·운영 Chromium 데스크톱/모바일 QA를 통과했습니다. 목격된 첫 절도는 경고, 두 번째는 추격으로 이어지는 규칙 동작을 검증했고, 배포 커밋 일치와 브라우저 오류 0건을 기록했습니다.", + "evidenceEn": "The public production runs `/boss`, `/demo` and `/evaluation` without an external AI API. Real Chrome QA records a legitimate 10-counter SENTINEL victory and an unattended defeat; a saved memory cartridge moves from SENTINEL to WRAITH while HP and round state reset and only predictor weights persist. In a separate MaleCNS test, final-frame-only scoring is 30/90 (33.3%) while temporal state from 36 calibrated optic cells plus a 111-parameter external readout reaches 66/90 (73.3%); the connectome-weight SHA is unchanged before and after. BVAT-37 on 2026-10-13 adds a verified playable /npc public demo: 200/200 source tests, local and deployed Chromium desktop/mobile checks, and deterministic first-warning / second-pursuit witnessed-theft behavior. Production /npc and its source commit identity were verified; zero JavaScript errors were reported in the dated run.", + "resultKo": "결과는 단순 AI 데모가 아니라 플레이 가능한 3D 시스템, 재현 가능한 평가, 명시적 권한 경계, 기억 이식 실험과 고정 connectome readout 연구를 한 공개 케이스 스터디로 묶은 것입니다. NPC 실험은 단일 규칙 기반 브라우저 시뮬레이션으로, 학습된 사회적 기억·LLM 대화·실제 멀티플레이 권한이나 사람의 재미 검증을 뜻하지 않습니다.", + "resultEn": "The result is a public case study that joins a playable 3D system with reproducible evaluation, explicit action boundaries, compatible memory transfer, and fixed-connectome readout experiments instead of presenting a neural model as a black-box demo. The NPC experiment is a single rule-based, session-local browser toy—not learned social memory, LLM dialogue, real multiplayer authority, or human-validated game quality." + } } ] } diff --git a/content/resume-manifest.json b/content/resume-manifest.json new file mode 100644 index 0000000..9e85df1 --- /dev/null +++ b/content/resume-manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "snapshotAt": "2026-09-14", + "files": [ + { + "file": "Sihyeong-Lee-Resume.pdf", + "sha256": "ab4933e15c7516cf455d0547daf4ba4b766c8d1d06a4fddfd8b4ab50a28b6b64" + }, + { + "file": "Sihyeong-Lee-Resume-DevEx.pdf", + "sha256": "9850dc45ed639d0e0998876ea2983bd3ee092c6f88ff45d3a62a9f30f3c4449f" + }, + { + "file": "Sihyeong-Lee-Resume-FDE.pdf", + "sha256": "406ab1da5b66058b831530ad3b99d46a4299586207a586665e7aba5db9919350" + } + ] +} diff --git a/content/verified-demos.json b/content/verified-demos.json new file mode 100644 index 0000000..6a7ff8f --- /dev/null +++ b/content/verified-demos.json @@ -0,0 +1,23 @@ +{ + "schemaVersion": 1, + "demos": [ + { + "project": "brain-vat-siege", + "verifiedAt": "2026-10-13", + "sourceCommit": "854213620980fe9c62ea72c12c8d58ec93c9c2e9", + "deploymentStatus": "public", + "demoUrl": "https://brain-vat-siege.vercel.app/npc", + "image": "/images/projects/brain-vat-npc-production.webp", + "titleEn": "NPC Living World — witnessed actions change the guard", + "titleKo": "NPC Living World — 목격한 행동에 따라 달라지는 경비", + "descriptionEn": "Playable Three.js example: a deterministic, rule-based guard receives filtered observations, warns after the first witnessed theft, and pursues after the second. The local-browser game engine remains authoritative for movement, events, collision, and allowed NPC intents.", + "descriptionKo": "직접 조작할 수 있는 Three.js 데모입니다. 규칙 기반 경비 NPC는 제한된 관측만 사용하며 첫 목격 절도에는 경고, 두 번째에는 추격합니다. 이동·이벤트·충돌·NPC 행동 허용 여부는 브라우저의 게임 엔진이 결정합니다.", + "testLineEn": "200/200 source tests; October 13 desktop/mobile production Chrome QA passed with no recorded JS errors.", + "testLineKo": "소스 테스트 200/200, 10월 13일 실제 운영 데스크톱·모바일 Chrome QA 통과 및 JS 오류 기록 없음.", + "caveatEn": "This is one deterministic NPC in a browser sandbox. It has no trained policy, LLM, generative dialogue, persistent social memory, real multiplayer authority, or human-validated play quality.", + "caveatKo": "브라우저 샌드박스의 규칙 기반 NPC 1명입니다. 학습된 정책·LLM·생성형 대화·영구 사회적 기억·실제 멀티플레이 권한·사람의 게임 재미 검증을 갖춘 것은 아닙니다.", + "captureNoteEn": "Cropped and resized from the actual 2026-10-13 public-production Chrome screenshot; source retained in the MAIN PC's BVAT-37 QA record. No private test data was used.", + "captureNoteKo": "2026-10-13 실제 공개 운영 Chrome 캡처를 위쪽 화면 중심으로 잘라 최적화했습니다. 원본은 MAIN PC BVAT-37 QA 기록에 보존되어 있으며 비공개 테스트 데이터는 사용하지 않았습니다." + } + ] +} diff --git a/docs/operations/evidence-workflow.md b/docs/operations/evidence-workflow.md new file mode 100644 index 0000000..4f1f9bd --- /dev/null +++ b/docs/operations/evidence-workflow.md @@ -0,0 +1,29 @@ +# Evidence workflow + +Plane Portfolio (FOLIO) is the execution source of truth. Source code and decision/evidence documents live here; local files are not a second task-status database. + +## Public facts versus collection + +- `content/evidence.json`: reviewed source snapshots with version/date/scope, pinned commit and SHA-256. +- `npm run evidence:check`: checks publication metadata, display/measurement consistency, résumé manifest and exact PDF bytes. Offline/CI safe. +- `npm run evidence:collect`: reads only allowlisted DevDeck/citefirst documents, verifies pinned blobs, reports later committed and uncommitted worktree candidates into ignored `.qa/evidence-candidates.json`. **Public content is never modified.** +- `npm run evidence:verify`: verifies pinned source bytes and parsed measurements against local repositories. Set `PORTFOLIO_SOURCE_ROOT` to their parent when they are not siblings. +- `npm run evidence:check -- --require-current-review`: also fails after the review deadline. Elapsed time does not change a historical measurement. + +The collector does not run providers, source-repository tests, Git pushes or deployment. Its parsers fail on unknown upstream formats. Before accepting a candidate, inspect raw runs, version, sample denominator, skipped cases and method, then edit the public manifest in a reviewed change. A source check is not human visual approval. + +## Verification and release + +1. Run type/lint/unit/format/evidence checks and build production assets. +2. Start production preview: `npm start -- -p 3100 -H localhost`. +3. Run `npm run qa:portfolio` and both locales of `qa:navigation`. `QA_BASE_URL` selects another preview; no live account is needed. +4. Preserve `.qa/portfolio/report.json` and screenshots. Its source fingerprint includes uncommitted source files. +5. Report automated QA, image review, production deployment and physical-device testing separately. A captured image is not human approval. A passing source build is not public deployment. + +Vercel analytics endpoints load only on a Vercel production deployment at the canonical public host. Third-party telemetry stays off on local/Tailnet previews. A public self-host may opt in with `PORTFOLIO_ENABLE_TELEMETRY=1`; this does not load Vercel-only endpoints. + +Public résumé PDFs remain a byte-verified **2026-09-14 document snapshot**. Their historical DevDeck numeric wording should be refreshed through the notebook-first hangar workflow. A portfolio collector must not silently rewrite authoritative job-search documents. + +### Local origin constraint + +On the checked Next.js 16.3.3 Windows build, a server explicitly bound to 127.0.0.1 rewrote Korean paths through a localhost origin and entered a self-redirect loop. An initial localhost probe reached an older preview on that port, so that result was not accepted as candidate evidence. On a fresh, isolated port, **both binding and requests set to localhost** returned 200 for EN/KO routes. Browser scripts, CI startup and probes therefore consistently use localhost, which remains loopback-only. This is a local preview configuration finding; the review does not claim a production redirect defect. diff --git a/docs/research/2026-10-12-ai-commoditization-positioning.md b/docs/research/2026-10-12-ai-commoditization-positioning.md new file mode 100644 index 0000000..36af205 --- /dev/null +++ b/docs/research/2026-10-12-ai-commoditization-positioning.md @@ -0,0 +1,48 @@ +# AI adoption is not the differentiator — portfolio positioning review + +Reviewed: 2026-10-12 (America/Los_Angeles) +Execution: Plane FOLIO-35/46/47, source branch `feat/reconcile-evidence-20261011` +Status: editorial and source-work review; not proof of public deployment, hiring outcomes, customer adoption, or independently measured business impact. + +## Premise + +Using a coding assistant, agent, MCP server, or model is no longer a distinguishing résumé headline. The harder questions a hiring manager or customer can evaluate are: what production or workflow problem was solved; what constraints were chosen; how correctness and failure paths were measured; who accepts release risk; and whether the stated outcome can be independently inspected. + +### External context, with sampling limits + +- Stack Overflow Developer Survey 2026, [AI overview](https://survey.stackoverflow.co/2026/ai): 73% of the survey's respondents were reported as daily AI users; coding assistants and agents were widely reported. The survey is self-selected, not a census of all engineers or an individual employment-market ranking. +- The survey's [trust question](https://survey.stackoverflow.co/2026/ai/data/ai-trust) (`n=14,304`) reports that **48.0%** trust AI when output is easy to verify, while **6.6%** trust it for many tasks *including important work decisions*. Do not paraphrase this as “everyone distrusts AI” or “93% distrust code.” +- Its [knowledge / attribution section](https://survey.stackoverflow.co/2026/knowledge) reports that 93% considered information sourcing at least somewhat important to confidence in AI-generated answers. Attribution is a survey finding, not measured effectiveness of this portfolio. +- The [2025 DORA State of AI-Assisted Software Development](https://dora.dev/research/2025/dora-report/) describes AI as an amplifier of an organization's existing strengths and weaknesses. It does not prove a specific developer's value. +- OpenAI's [Harness engineering](https://openai.com/index/harness-engineering/) (2026-02-11) describes a team in which human engineers shape intent, environments and feedback loops while agents write code. That is an example of a workflow model; it does not imply this site's author achieved its timing, scale or release metrics. + +Editorial decision: **put proof and reasoning before tool names**. No percentage from these external surveys is necessary on the public site; publish concrete project evidence instead. + +## Three portfolio stories with different evidence quality + +| Case study | Engineering judgment to show | Verifiable record and its limit | What not to claim | +| --- | --- | --- | --- | +| DevDeck | Ownership of state across coding agents, restart/reconnect recovery and conflict handling | `content/evidence.json` pins candidate **1.37.8** review: 1,289 passed, 1 skipped. Source commit and hash are recorded; not an installed release or current live test. | “1,289 tests passed today”, deployed reliability over months, enterprise adoption | +| citefirst | Retrieval evaluation rather than general AI/RAG enthusiasm | A committed **46-question** PostgreSQL 17 manual evaluation: recall@5 **0.978**, MRR@10 **0.834**. Search quality, *not answer correctness*. | Confusing private uncommitted 56-question experiment or valid citation IDs with factual-answer accuracy | +| Creator OS | Separating implementation/build success from release readiness: backups, data boundaries, OAuth/policy/operator gates | Project case study and source review describe a private-staging release with explicit unsatisfied external gates. Numbers on other source trees require their own dated evidence approval. | Public commercial launch, external customers, revenue, universal production-readiness guarantee | + +Studio / MingleStage / RailForge / STRATA / Music-MV show breadth of exploration. Label them as private, pilot, technical spike or development—not as business outcomes. A project count is an inventory count, not proof of quality or customer value. + +## Copy and UX contract + +1. Keep the distinctive `I build. I ship. I bury the dead.` identity. Under it, say how engineering output is accepted, not how many AI models are used. +2. Home: display two source-pinned evidence snapshots with dates, scope and limitations, plus direct project and immutable source links. The `PUBLIC LINKS` count should not imply commercial launch or active users. +3. Hire: lead with the customer's current workflow or problem, not “AI expert.” Show **problem definition → verification → release or deliberate stop**. Link each stage to an inspectable example and keep full-time vs paid project offers separate. +4. Ledger: public work, private experiments and active prototypes remain distinct. Descriptions and search metadata must not claim every build has a public URL. All case studies should contain problem, decision, evidence and result; missing evidence should remain explicitly unresolved. +5. Source honesty: a green source test is not a production release; skipped tests are not successes; a private staging screenshot is not public availability. Site credibility falls if any of these boundaries disappear. + +## Acceptance tests + +- EN/KO translation keys remain consistent; first-screen CTA, explicit evidence scope, real internal and Git commit links are tested in the existing browser matrix. +- New regression tests pin the three examples and guard against blanket public-release claims. +- Production build, type check, lint, unit, evidence checksum and browser tests must pass *on the final branch*. +- An unresolved dependency audit, privacy/authoritative résumé approval, and actual Vercel deployment remain **separate release gates**. PR #33 and PR #34 are draft until those are resolved and do not create an implied approval. + +## Follow-up research / measurable improvements + +Review target-job descriptions by actual employer/contract scope before declaring a globally optimal job title. Add a verifiable test artifact for one client-shaped workflow: a failure case, recovery proof, versioned test run, and concrete user-facing acceptance, rather than adding another model badge. Record a 2026-12-22 positioning review in FOLIO-35; do not claim it is an automatic reminder. diff --git a/docs/research/ai-positioning-2026-09-22.md b/docs/research/ai-positioning-2026-09-22.md new file mode 100644 index 0000000..f5ded70 --- /dev/null +++ b/docs/research/ai-positioning-2026-09-22.md @@ -0,0 +1,26 @@ +# AI positioning review — 2026-09-22 + +## Decision and limits + +Keep AI-native product engineering / agent systems as the portfolio specialization, backed by TypeScript/Python, product, backend and infrastructure work. This is an editorial fit decision, not a measured ranking of the whole hiring market. One live posting and three official engineering articles are a limited sample. Articles explain practice, not job-market share or the author's skills. + +## Sources checked + +- [OpenAI: AI Systems Engineer, Codex Agents](https://openai.com/careers/ai-systems-engineer-codex-agents-san-francisco/) — live posting checked 2026-09-22; publication date unstated. Requests harnesses, sandboxing, state, evals, observability and performance, alongside production engineering. It also requests low-level/ML capabilities; do not imply Rust, kernel or GPU-fleet expertise merely because some keywords align. +- [OpenAI: Harness engineering](https://openai.com/index/harness-engineering/) — published 2026-02-11. Reference for repository legibility, acceptance boundaries and inspectable testing. It does not establish universal productivity gains. +- [Anthropic: Demystifying evals for AI agents](https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents) — published 2026-01-09. Separates transcripts from actual end-state outcomes and deterministic, model and human grading. Supports checking artifacts instead of trusting a success report. +- [Anthropic: Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents) — published 2025-09-29. Foundation for context/tool selection, not a new September 2026 announcement. + +## Evidence mapping + +| Focus | Portfolio evidence | Limitation | +| --- | --- | --- | +| Tool use and agent workflow | DevDeck; studios MCP and persisted verification | Tool availability alone is not autonomous task success | +| Evals and evidence quality | citefirst committed 46-question record | 56-question local changes are uncommitted; LLM judgment is not deterministic truth | +| State and reliability | DevDeck host-owned metadata and recovery records | Candidate evidence is not installed release or physical-network acceptance | +| Production integration | ITEMBOX integrations/billing; GEOMEC infrastructure; Creator OS | Creator OS is unlaunched; do not invent customers, revenue or OAuth readiness | +| Cost and latency | A useful next case-study dimension | Require task, configuration, sample and run artifact before claiming measurements | + +## Next review + +Review by **2026-12-22**, earlier when target roles change. Sample several relevant employers and the user's actual preferred roles before changing weights. No recurring job was scheduled in this pass. Track due work in Plane; do not imply automatic review exists. diff --git a/docs/reviews/2026-10-11-live-site-refresh-audit.md b/docs/reviews/2026-10-11-live-site-refresh-audit.md new file mode 100644 index 0000000..3897ad6 --- /dev/null +++ b/docs/reviews/2026-10-11-live-site-refresh-audit.md @@ -0,0 +1,125 @@ +# WritingDeveloper live-site and project refresh audit — 2026-10-11 PT + +Status: **reviewed findings; implementation/release not approved by this document**. Existing uncommitted changes must be preserved. No claim that public deployment, visual acceptance, or Plane issue mutation has occurred. + +## Sources inspected + +- Live https://writingdeveloper.blog/ and public routes, HTTP response headers, GitHub repository / Actions. +- 4080 MAIN: `Documents/GitHub/portfolio`, selected adjacent repositories and their current README/Git states. +- Notebook (SIHYEONG-4080): selected creator-learning-os, studios, google-video-lab, music-mv-studio, soursea-site and hangar repositories. +- The existing Portfolio execution guide, 2026-09-22 source/evidence QA review and FOLIO workflow. + +## Verified production facts + +- Public response: HTTP 200; `Server: Vercel`, Next.js routing. **This does not establish Notebook as the HTTP serving host.** +- `/en`, `/ko`, both Hire pages, Studio, Projects, Blog, Play, sitemap, robots, llms.txt: HTTP 200. +- `/resume/Sihyeong-Lee-Resume.pdf`: **HTTP 404** on live at this review. The separate 4080 working tree has three dated PDF snapshots and a checksum manifest; this is a deployment/release discrepancy, not an automatically approved document publication. +- Observed response bodies without compression negotiation: `/en/projects` about 509 KB; `/en/play` about 273 KB; `/en` about 129 KB. These are **not** Lighthouse/Web Vitals measurements. Investigate streaming/server payload and client behavior before optimizing. +- Latest GitHub main observed: `689354e` (2026-09-27). Local MAIN working branch `fix/positioning-alignment-2026-09` has 36 modified/untracked paths; preserve it. +- Live indexed blog section exposes latest highlighted article dated 2026-08-27. The working tree contains an unpublished bilingual agent-engineering article; publication requires editorial and factual QA. + +## Verified quality/CI findings + +- Existing 4080 worktree (before any new edits here): TypeScript, ESLint, Prettier, 224 Vitest tests, evidence manifest/checksums, translation parity, and `next build` all passed on 2026-10-11. +- Open GitHub PR [#32](https://github.com/writingdeveloper/portfolio/pull/32) attempted Next.js 16.3.8 but [GitHub CI run](https://github.com/writingdeveloper/portfolio/actions/runs/37216106114) failed in `npm ci` because `yaml@2.9.1` was missing from package-lock. **No code-test result can be inferred for that PR.** +- Dependency repair is isolated in `../portfolio-security-20261011`, branch `fix/next-security-lockfile-20261011` from GitHub main. Follow the clean-install, type/lint/test, production build and CVE/audit gates before any merge. + +## Project evidence and public-content decisions + +| Candidate | Source checked | 2026-10-11 interpretation | Public treatment | +| --- | --- | --- | --- | +| Creator Learning OS / Creator OS | Notebook creator-learning-os README, 2026-10-07 commit | Backup restore QA documented; customer OAuth and external launch gates remain | Existing entry: update only with source-backed milestones, not launch claims | +| Studio / Studio Gallery | Notebook studios 2026-10-11 commit, working state | Active five-modality production tooling; substantial uncommitted work | Refresh case study after representative screenshots, privacy and model-output approval | +| MingleStage / CrowdStage | MAIN README and staged release notes | Invitation-pilot, open-signup release gates outstanding | Honor prior FOLIO-33 deferral; do not promote as generally launched | +| RailForge | MAIN local research-first README, many uncommitted changes | Browser railway-game R&D | Draft engineering-log case study, label prototype/research | +| Project STRATA | MAIN Unreal spike README | Exploration-game technical spike, runtime/vertical-slice acceptance open | Blog engineering diary candidate, not shipped flagship | +| OpenMontage Reference Lab | MAIN 2026-10-11 latest commit | Video analysis, generation and face-flicker QA experiments | Private/prototype; do not publish personal media or third-party faces by default | +| Music/MV Studio, YuE2, VOCALOID MCP | Notebook and MAIN READMEs | Audio/music toolchains, work-in-progress licensing/output acceptance | Architecture/lessons only after license and attribution review | +| Google Video Lab | Notebook README, 2026-10-03 benchmark commit | Flow/Veo benchmark with private mirror | Research post candidate with redacted benchmark evidence | +| Soursea LLC site | Notebook soursea-site README, 2026-10-11 commit | Separate corporate website on Cloudflare Worker | Cross-link only where audience and brand are appropriate | + +This is a candidate inventory, not a declaration of commercial readiness, users, revenue or successful publication. + +## Proposed FOLIO execution mapping (not synced to live issues yet) + +1. **P0 — FOLIO-36 continuation:** verify résumé content/approval and deploy reviewed changes; check public PDF MIME/SHA, all locale CTAs, rollback. Keep human review separate. +2. **P0 — new security task (check live FOLIO board for duplicate):** resolve PR #32 broken dependency lock, prove clean `npm ci` and CI, merge with dependency-review gate. +3. **P1 — FOLIO-34 continuation:** reconcile MAIN uncommitted feature/evidence work with GitHub main; review localized article and original résumé snapshots. No force push. +4. **P1 — new curation task:** classify recent projects into shipped/pilot/prototype/private, require dated source screenshot and claim-level evidence before publication. +5. **P1 — new UX task:** review Hire CTA and mobile conversion path; test real public endpoints and email links with browser. +6. **P2 — new performance/SEO task:** profile projects page payload, font preloads, image priority, structured data, crawlability; record baseline and after metrics, avoid speculative LCP claims. +7. **P2 — FOLIO-35:** preserve next 2026-12-22 AI positioning review; no automatic market claim. + +## Release guardrails + +Keep changes in isolated branches; do not force-push, expose secrets, auto-publish resumes, launch an invitation pilot as public, or overstate AI/quality metrics. Require immutable source evidence, automated QA, human visual review, physical-device review where applicable, production verification and rollback separately. + +## Plane synchronization state + +Existing workspace `soursea`, project `FOLIO` confirmed by prior registry / repo guide. The project registry was refreshed on the MAIN PC; **issue-level live read/write was not confirmed in this pass**. This report is a source-of-evidence handoff, not a substitute for writing/updating the Plane work items. + +## Same-day security implementation addendum (2026-10-11 PT) + +- Isolated from the 36 pre-existing local WIP paths: `C:\\Users\\SIHYEONG\\Documents\\GitHub\\portfolio-security-20261011`. +- Implemented Next.js 16.4.0 + synchronized npm lockfile. Resolved earlier GitHub Dependabot #32 failure due to missing `yaml` lock entry. Applied compatible transitive patches via `npm audit fix` **without** `--force`. +- GitHub branch `fix/next-security-lockfile-20261011`, commit `bbd749b` pushed. +- Opened **draft** PR [#33](https://github.com/writingdeveloper/portfolio/pull/33), not merged/deployed. +- Verified clean `npm ci` on original security candidate; final repaired lock `npm ci --dry-run` PASS; types, lint, formatting, 197 tests (21 files), i18n parity, production build and `git diff --check` all PASS. This branch starts from public GitHub main and therefore has fewer tests than 4080's uncommitted WIP 224-test branch. +- `npm audit --omit=dev --audit-level=high` still reports **3 high advisories** from `braces` dependency chains (`@keystatic/next` / `chokidar`, Next ESLint tooling). Complete audit reports 7 high. The suggested `npm audit fix --force` would break Keystatic compatibility, so it was intentionally not used. +- GitHub's security alert on default main requires separate review; this source task does not attest that vulnerabilities are eliminated. +- Pending: GitHub Linux CI on draft PR #33, security exception triage, preview browser QA, review and merge, actual Vercel deployment and live smoke test. Do not confuse successful local `next build` with production release. + + +## Linux GitHub CI read-back for draft PR #33 + +- [Run 38195978378](https://github.com/writingdeveloper/portfolio/actions/runs/38195978378): **FAIL overall**, but successful separately: checkout/setup, `npm ci`, type checking, lint, formatting, unit tests, translation parity, hero-image verification and production build. +- Sole failing step: **Audit dependencies**, due to the remaining `braces` advisory (7 High in full dependency audit). That security gate was **not bypassed**; PR remains draft and unmerged. +- This confirms the lockfile/clean-install problem from #32 is resolved in GitHub Linux CI, independently of local Windows checks. + +## Live visual first-viewport review (public routes) + +Captured actual Chromium screenshots at 2026-10-11 PT, saved under ignored `.qa/portfolio/live-20261011/`: + +- `desktop.png`: `/en` at 1440×900. +- `mobile.png`: `/` at 390×844 (Korean). +- `hire-mobile.png`: `/en/hire` at 390×844. +- `projects-desktop.png`: `/en/projects` at 1440×900. + +**Observations (review candidates, not measured conversion or accessibility failures):** + +1. The brutalist/dark editorial visual identity is consistent, readable and distinctive. Preserve it instead of redesigning indiscriminately. +2. English desktop home first viewport is almost entirely the large typographic hero with major open space to the right; no clearly visible work/evidence/contact CTA above fold. Consider a concise featured proof or CTA with A/B and mobile review, without diluting the visual theme. +3. Korean mobile home exposes six tightly spaced textual nav targets across the top; English mobile Hire uses a hamburger/settings language header. Review route-consistent small-screen navigation and actual interactive target geometry against WCAG; screenshots alone do not establish failure. +4. English mobile Hire presents full-time positioning and explanatory text first. The résumé/contact action is not visible in the initial 390×844 viewport. Ensure at least one unambiguous, functional contact/resume CTA is reachable promptly and the public PDF path returns 200 with reviewed content before surfacing the link. +5. Projects desktop selected-work cards have very dense multi-paragraph copy and stacks of skill tags; Studio card lacks a visible screenshot compared to DevDeck. Propose concise 2–3-line outcomes plus scannable evidence/roles/status, with expandable case-study detail. Do **not** invent production scale, release status, or adoption. + +No full-page/mobile interaction or physical-device acceptance is asserted by this static viewport review. Browser screenshots were taken from **public Vercel production**; local uncommitted visual changes were not used. + +## Follow-up implementation — 2026-10-11 21:45 PT + +This continued user-approved maintenance pass is staged, **not yet deployed**. Public site remains the earlier Vercel release. + +### Live Plane (confirmed via authenticated REST) +- Read 44 pre-existing FOLIO work items, including FOLIO-34 QA, FOLIO-35 Todo and FOLIO-36 Todo. +- Created FOLIO-45 urgent/In Progress for the strict security audit and Next release, FOLIO-46 high/Todo for source-backed project curation, FOLIO-47 high/Todo for Hiring UX and FOLIO-48 medium/Todo for measured performance/SEO. +- Added source QA/release read-back comments to FOLIO-34 and FOLIO-36. Do not mark release Done before production evidence exists. + +### Source integration +- Preserved the original `portfolio` checkout and all 36 modified/untracked paths. Created a separate `portfolio-reconcile-20261011` Git worktree, branch `feat/reconcile-evidence-20261011`. +- Snapshotted evidence/resume work from WIP and merged current public GitHub main `689354e`. Five content/test conflicts were manually reconciled, preserving the AI/agent engineering identity and the later paid-work-oriented Hire case studies, with remote-first and LA hybrid/on-site eligibility accurately stated. +- The project ledger now has 48 entries after adding **RailForge, Project STRATA, Music / MV Studio** as `building` / `private` research and development; bilingual evidence sections clearly decline a shipped/public claim. Existing selected-work and project release labels were left intact. +- Hero positions early résumé/portfolio/AI-pipeline links within the first viewport while retaining the editorial visual identity. The Hire first viewport has contact email and résumé options. Mobile home navigation targets are given a min-height. Project listing card copy and ProjectList JSON-LD use bounded, non-destructive excerpts; full technical details remain on linked case-study pages. +- Added browser acceptance for visible Hire contact button/anchor in each viewport; added tests for guarded summary behavior and private-prototype classification. +- Integrated security branch `fix/next-security-lockfile-20261011` (Next.js 16.4.0 + compatible transitive patches) into the candidate, so the final release candidate does not regress to the older vulnerable Next.js. + +### Verification (progressively confirmed, do not conflate versions) +- **Before integrating Next.js 16.4.0**, the reconciled Next 16.3.3 branch passed 226 Vitest tests, TypeScript, ESLint, Prettier, evidence/PDF hashes, translations and production build. Chromium QA passed EN/KO × 1440/390 × dark/light, three versioned PDF byte hashes, and both locale navigation suites. Local candidate /en/projects/railforge, /project-strata and /music-mv-studio returned HTTP 200. Real first-viewport desktop and mobile screenshots were reviewed and CTA placement looked correct. All previews stayed loopback-only. +- The fresh Next 16.4.0 build, security audit, updated browser acceptance and production recheck are a **separate** final gate. The upstream `braces` high vulnerability is still unresolved; see `2026-10-11-next-braces-dependency-gate.md`. +## Final localhost candidate QA — 2026-10-11 21:51 PT + +- The isolated candidate merged Next.js 16.4.0 and its compatible dependency patches from draft PR #33. Clean npm installation succeeded. +- Current source: TypeScript, ESLint, Prettier, 228 unit tests (23 suites), evidence/PDF checks and bilingual posts (11/11) PASS. Optimized build PASS (148 prerender tasks). +- Chromium review on Next.js 16.4.0: all eight EN/KO × 1440/390 × light/dark combinations PASS. All three résumé PDFs have valid MIME type and exact expected SHA-256. New prototype case-study links respond HTTP 200; no unexpected browser errors. +- Bounded project excerpts are rendered in catalogue cards and summary JSON-LD; full technical copy stays on detail pages. +- One read-only lab snapshot at `.qa/portfolio/performance-20261011.json`: /en/projects public decoded HTML 509,187 bytes; localhost candidate 484,503 bytes. Public compressed document transfer about 47 KB vs local about 66 KB because origins and compression differ. These are **different content versions and server environments**, not comparable Core Web Vitals, controlled A/B, or proof of deployed speed gains. The lab sampled TTFB/FCP/LCP; tested views had no horizontal overflow. +- STRICT dependency audit still fails on the unpatched braces advisory. PR #33 remains draft, and Vercel production has not been updated. Preserve release gate FOLIO-36. diff --git a/docs/reviews/2026-10-11-next-braces-dependency-gate.md b/docs/reviews/2026-10-11-next-braces-dependency-gate.md new file mode 100644 index 0000000..ad43e0c --- /dev/null +++ b/docs/reviews/2026-10-11-next-braces-dependency-gate.md @@ -0,0 +1,31 @@ +# Remaining dependency security gate — 2026-10-11 (America/Los_Angeles) + +## Status and scope + +The latest compatible maintenance candidate is GitHub draft [PR #33](https://github.com/writingdeveloper/portfolio/pull/33), Next.js **16.4.0** plus synchronized lockfile, `sharp@0.35.5`, `source-map-js@1.2.2`, and `brace-expansion@2.1.7`. GitHub Actions Linux passed `npm ci`, typecheck, lint, format, unit, bilingual parity, hero verification, and build. The only red step is the unchanged strict dependency audit. + +## Outstanding advisory + +[GHSA-vfj7-8cjw-p6xm](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm) concerns malicious/deeply nested brace glob patterns that could exhaust the JS stack. `npm view braces version` reported **3.0.3** on 2026-10-11; no officially patched version was available. `npm audit --omit=dev --audit-level=high` reported 3 High entries from this dependency group; full `npm audit --audit-level=high` reported 7 High. Grouped alerts are **not necessarily seven independently reachable exploit paths**. + +The dependency paths observed in the patched lockfile are: + +- `@keystatic/next@5.0.5 → chokidar@3.6.0 → braces@3.0.3` (CMS/tooling, installed via production package). +- `eslint-config-next@16.4.0 → @next/eslint-plugin-next → fast-glob@3.3.1 → micromatch@4.0.8 → braces@3.0.3` (lint/build tooling). + +The website uses local file-backed content with a configured Keystatic admin UI. Production access guards exist for `/keystatic` and `/api/keystatic` in local storage mode; **this is a route-level observation, not proof that every vulnerable call path is unreachable**. No arbitrary user-submitted glob pattern was identified in portfolio request handlers during this source review; full security threat modeling remains outstanding. + +## Disallowed shortcuts + +- Do **not** use `npm audit fix --force` for this dependency: npm offered `@keystatic/next@0.0.6`, a breaking and incompatible downgrade. +- Do **not** claim `npm audit` passed; it did not. +- Do **not** globally disable audits, blindly omit production dependencies, or publish a fabricated patched version. + +## Remaining path to release + +1. Review advisory against actual input sources and affected internal call sites, including Keystatic import/runtime paths; record threat model and exposure. +2. Seek upstream maintained release or carefully reviewed compatibility-preserving patch/vendor route. Re-run all unit, integration, browser, and clean-CI checks afterward. +3. If no compatible fix becomes available and an earlier release is still required, seek an **explicit, time-limited security risk decision** with reason, scope, mitigation, owner, and expiry. A blanket request for general maintenance is not such acceptance. +4. PR #33 and its stacked portfolio refresh remain draft until gate resolution. Vercel production deployment and public résumé verification are separate FOLIO-36 acceptance. + +Plane: FOLIO-45 (dependency gate), FOLIO-36 (actual release). diff --git a/docs/reviews/2026-10-12-ai-era-portfolio-qa.md b/docs/reviews/2026-10-12-ai-era-portfolio-qa.md new file mode 100644 index 0000000..0d5cb59 --- /dev/null +++ b/docs/reviews/2026-10-12-ai-era-portfolio-qa.md @@ -0,0 +1,34 @@ +# AI-era portfolio positioning and browser acceptance — 2026-10-12 PT + +Branch: `feat/reconcile-evidence-20261011` (stacked draft PR #34 over blocked security PR #33) +Host/source: MAIN 4080, separate worktree `portfolio-reconcile-20261011` +Status: candidate source verified locally; public release remains **blocked** + +## Goal + +AI-tool usage is common among software engineers. Demonstrate human engineering responsibility and verifiable outcomes rather than presenting Claude, Codex, model counts or generation volume as unique value. Source research and limitations: [2026-10-12-ai-commoditization-positioning.md](../research/2026-10-12-ai-commoditization-positioning.md). + +## Implementation + +- Home hero paragraph uses the same translated source as SEO messaging and explicitly distinguishes AI-assisted code from acceptance criteria, failures and release safety. The original ledger branding is preserved. +- Home `#verified-work` contains both snapshots from `content/evidence.json` (DevDeck candidate and citefirst committed retrieval evaluation) with source timestamps, scope, unverified conditions, project links and pinned Git commit links. Metrics were **not changed** by this update. +- The ledger's `PUBLIC LINKS` label counts public destinations without claiming that these are commercial launches; the page clarifies that builds include unfinished prototypes, not customers or revenue. +- The Hire `#delivery-method` section shows **frame problem → verify outcome → ship or block**, each with a concrete project-case link: DevDeck, citefirst and Creator OS. Copy avoids implying that code generated by an agent is itself a production outcome. +- EN/KO metadata and visible projects catalogue descriptions distinguish public links from private or unfinished work and eliminate earlier blanket demo-link claims. +- Added five regression tests and browser assertions that validate render presence, proof count/source pins, source limitations, and the three linked case studies. + +## Local check read-back + +- TypeScript, ESLint, Prettier: PASS. +- Vitest: **233 passing tests / 24 files** (compared with 228 before this change). +- Source evidence structure and all versioned résumé PDF hashes: PASS. +- Bilingual posts: **11/11 pairs**, no unmatched translations. +- Next.js 16.4.0 production build: PASS; **148 prerendered static-generation tasks**. +- Browser source build on localhost-only port 3193: EN/KO × desktop 1440px/mobile 390px × dark/light, **8/8 PASS**, with no unexpected console/page/network errors. Dated résumé PDFs: 3/3 MIME and SHA-256 PASS. EN and KO navigation suites both PASS. +- Captured EN/KO desktop/mobile screenshots of `#verified-work` and `#delivery-method` under ignored `.qa/portfolio/ai-evidence-20261012/`. Screenshot review: legible two-column proof on desktop and stacked proof/method cards on mobile, no measured horizontal overflow. A sticky header can overlay an element screenshot during automatic element-scroll; the method section now has a scroll-margin offset. This is not physical-device/human launch acceptance. + +## Open gates + +- `npm audit --audit-level=high`: **nonzero**, seven High findings due to upstream unpatched `braces` dependency paths. `npm view braces version` on 2026-10-12 returned **3.0.3**; not a fix. No unsafe `--force` downgrade, blanket audit bypass, or acceptance of residual risk was applied. +- The original 4080 portfolio worktree has been left untouched. Vercel public `writingdeveloper.blog` still serves its prior release; public résumé PDF path remains a known 404 until FOLIO-36 has a reviewed merge, deployment and live hash/MIME/link verification. +- Pinned metrics have historical scope, and curated project's commercial outcomes remain **unmeasured**. Editorial review, external hiring-market validation, security remediation and production release are separate decisions. diff --git a/docs/reviews/2026-10-12-devdeck-incident-case-study.md b/docs/reviews/2026-10-12-devdeck-incident-case-study.md new file mode 100644 index 0000000..b600633 --- /dev/null +++ b/docs/reviews/2026-10-12-devdeck-incident-case-study.md @@ -0,0 +1,42 @@ +# DevDeck incident reconstruction — UI convergence after a remote rename + +Reviewed: 2026-10-12 (America/Los_Angeles) +DevDeck source: [baseline `b781754`](https://github.com/writingdeveloper/devdeck/commit/b7817547dd2100751ce46229a83810319affe82b) → [fix `fa88648`](https://github.com/writingdeveloper/devdeck/commit/fa8864833b98e7d81f140e598b1f56d4d2b60c39). +Publication: **1.37.8 candidate / source and CI verified, NOT a public release**. + +## User-visible defect and the actual evidence + +A remote DevDeck viewer renamed a running terminal. The host process's live runtime metadata showed that rename but the host's visible header and sidebar retained the old name. The original acceptance check inspected host metadata only and therefore missed the user-visible failure. After adding a real three-Electron-app harness, the historical baseline's first UI convergence assertion **timed out**. + +Source: `devdeck/docs/session-sync-review-2026-09-08.md` at `fa88648`. The historical local `devdeck/qa/shots/session-sync-before/results.json` contains an empty passed list and one `TimeoutError: page.waitForFunction: Timeout 8000ms exceeded`. This historical reproduction was documented in September; **it was not rerun on the baseline on 2026-10-12**. + +Root cause: `syncAdoptedLabel` in host renderer excluded `LOCAL_MACHINE_ID` label announcements, leaving host-visible state disconnected from the runtime update. Cached titles could be mistaken for authority; unversioned fire-and-forget writes, weak session identity and stale async responses introduced additional consistency risks. + +Candidate changes: session identity scope (machine + project + provider + conversation, plus terminal incarnation); a host-owned title transaction validating authorization and expected revision, persisting to disk before runtime event/ACK; caller-visible version conflicts preserving drafts; stale-pull invalidation and read-only viewer reattachment; legacy method refusal. + +This is a specific software incident, not a broad claim that an AI coding agent wrote or fixed the bug unaided. + +## Verified now, 2026-10-12 + +All commands ran on **MAIN / SIHYEONG-MAIN**, with the source checkout at exact `fa8864833b98e7d81f140e598b1f56d4d2b60c39` and **no dirty changes**. + +- `npm test -- src/main/sessionLabels.test.ts src/shared/sessionConsistency.test.ts`: **20/20 passed**. The storage-failure test intentionally exercises an EISDIR write error and asserts no false runtime/peer commit; the expected error is a controlled test fixture, not a failed test. +- `npm run qa:session-sync`: built the Electron app and passed **8/8** host + two-viewer real loopback TLS user journeys. These include UI+disk convergence, no remount, stale edit conflict, disk failure/retry, permission removal, offline/reconnect, restart/cache clear and repeated rename cycles. +- `devdeck/qa/shots/session-sync/results.json`: 8 pass strings, `errors: []`. Its locally recorded SHA-256 at this run was `c105102941be924c13d7d64b894ec1584c617b8140bed679782d85a281388414`. +- Source [same-fix-commit Linux/macOS/Windows CI](https://github.com/writingdeveloper/devdeck/actions/runs/34319241204) was green. The Windows failure-and-recovery job includes the real host + two viewers exercise. That GitHub action belongs to `fa88648`, not any more recent/default-branch commit. + +### Real screenshot provenance and privacy + +- Original baseline fixture: `devdeck/qa/shots/session-sync-before/Build-workstation-failure.png`, SHA-256 `7b94611de24c39498ee834eb96becd5d7f93472dfc6146a4e56d3ce9b48ee5ca`. +- Original current passing fixture: `devdeck/qa/shots/session-sync/Build-workstation.png`, SHA-256 `cda5daae66e9a390199430a88469b0c98da69720003c11fd93105a0a4621f6e2`. +- Public derivative assets: `public/images/projects/devdeck-sync-qa-before.webp` and `...after.webp`. Each **composites two real crops of the same corresponding screenshot**: host title and sidebar. The terminal area containing a local user/profile path was excluded. Crops are labeled as QA fixtures, not production installations. The before and after images were captured on different dates. + +## Disclaimers and release gates + +This is **one Windows machine with three isolated Electron processes**, temporary profiles, fixture repository and harmless CLI; it is not two physical computers with changing VPN/wifi/sleep. It does not validate months of field use, live paid-agent workloads, installer/updater approval, or user acceptance. Notes remain on separate last-write-wins semantics. DevDeck 1.37.8 still has an unreleased PR; do not call it publicly shipped. + +The WritingDeveloper site embeds this **incident case study** inside the existing DevDeck project detail, adds bilingual source/CI links, and navigates directly from Home and Hire. Separate FOLIO-45/36 security/release gates prevent publishing the broader portfolio update to Vercel production; green local site tests are not release authorization. + +## Site QA contract + +`content/failure-cases.json` holds the dated incident, exact source commit SHA, bounded result counts and limitations, plus six immutable GitHub source links. `src/lib/__tests__/devdeck-failure-case.test.ts` enforces dates, links, fixture image paths and that private paths aren't linked. `npm run qa:portfolio` additionally checks actual EN/KO DevDeck pages, the two loaded QA crops, source links, test counts and visible scope caveats. diff --git a/docs/reviews/2026-10-13-portfolio-refresh-audit.md b/docs/reviews/2026-10-13-portfolio-refresh-audit.md new file mode 100644 index 0000000..3c8973f --- /dev/null +++ b/docs/reviews/2026-10-13-portfolio-refresh-audit.md @@ -0,0 +1,81 @@ +# WritingDeveloper — production-to-source refresh audit (2026-10-13 PT) + +**Scope:** public `https://writingdeveloper.blog`, 4080 MAIN portfolio source and companion projects, Notebook project evidence, GitHub PRs/CI, Soursea FOLIO Plane board. + +**Important status:** This document records observations and candidate code changes. **It does not approve or assert a Vercel production deployment**. Current live site serves the earlier release, while the updates live in draft PR #34 stacked on draft security PR #33. + +## Priority answer + +**Release verified work before adding more projects.** The most expensive present defect is the broken résumé. Second is the unapproved dependency-security update. Third is stale/ambiguous project claims. Shipping ten more experimental projects would not fix the basic ability of a recruiter or customer to verify the work and contact its author. + +## 1. Public availability, links and navigation + +- Public `/`, `/en`, `/hire`, `/en/hire`, `/projects`, `/en/projects`, Studio, Blog, robots, sitemap and machine-readable pages returned HTTP 200 at review. +- All **three public résumé PDFs returned HTTP 404**: + - `/resume/Sihyeong-Lee-Resume.pdf` + - `/resume/Sihyeong-Lee-Resume-DevEx.pdf` + - `/resume/Sihyeong-Lee-Resume-FDE.pdf` +- The full discovery sweep evaluated **64 raw targets**: 57 HTTP 200, six 404 and one timeout. Three 404s belonged to private GitHub repositories whose source URLs are deliberately not shown as public links. Do **not** call those three customer-visible broken links. +- Permanent `npm run audit:public-links` now filters the public surface: **56 targets, 52 reachable, 3 internal PDF 404, 0 external HTTP errors, 1 inconclusive**. The inconclusive domain is `https://kindling.writingdeveloper.blog`, which timed out on both a Python 11-second read and independent 22-second GET/15-second HEAD. This does **not** establish a permanent shutdown. Recheck origin/DNS/health and stop advertising availability if it remains unreachable. +- Audit report is stored in ignored `.qa/portfolio/public-link-audit-latest.json`. Run `npm run audit:public-links -- --all-projects` for both-locale detail routes as well. It is read-only; internal HTTP 400+ produces a failing exit so it cannot be casually described as green. Externally blocked requests are separately reported as warnings. +- The live sitemap's XML contains 220 `loc` values, **including image URLs and alternate links**. Do not interpret that as 220 unique indexable pages or SEO reach. + +## 2. GitHub/release state + +- GitHub public `main` continues to serve the older production source. A direct HTTP `Server: Vercel` response establishes Vercel as the public serving layer, not Notebook as the website origin. +- [PR #33](https://github.com/writingdeveloper/portfolio/pull/33) upgrades Next.js and repairs a lockfile; [PR #34](https://github.com/writingdeveloper/portfolio/pull/34) stacks 4080 evidence, localized UX, DevDeck failure case and now the October project corrections on it. Both are **draft, unmerged**. +- Latest observed `npm audit --omit=dev --audit-level=high` remains nonzero, **three High entries**, due to unpatched `braces` inherited via `@keystatic/next → chokidar`. The latest compatible `braces@3.0.3` was **not** an upstream fix. `npm audit fix --force` would downgrade Keystatic incompatibly. Do not globally disable the audit or call those risks accepted without explicit scope, owner and expiry. +- An independent Dependabot [PR #35](https://github.com/writingdeveloper/portfolio/pull/35) was created for `brace-expansion@1.1.21`; its GitHub CI failed in `npm ci`. That is **not** the same `braces` problem and cannot silently unblock #33. +- On the source preview in prior sessions, TypeScript, lint, format, unit tests, images, actual Playwright EN/KO mobile/desktop/light/dark and all three local résumé file hashes have passed. Each new source commit needs its own final CI read-back before release. +- **FOLIO-45** owns security decisions; **FOLIO-36** owns editorial PDF acceptance, merge, Vercel production, rollback and real public link/hash/CTA smoke QA. Neither is Done. + +## 3. Content accuracy — what was updated in draft PR #34 + +| Item | Prior issue | Verified evidence as of this audit | Candidate action | +|---|---|---|---| +| **BRAIN//VAT** | `updatedAt=2026-09-23` and no NPC experiment | Local git `8542136` / `d28291e`, BVAT-37 2026-10-13 public production Chrome desktop/mobile PASS, Node 200/200, deterministic rule guard. Live `/npc` 200 | Date set to Oct 13; descriptions/evidence updated to distinguish a rule-based single NPC from any learned model, LLM or multiplayer engine. Added optimized public-production Chrome screenshot and demo panel | +| **Creator OS product** | Ledger said OAuth unfinished, obscuring restricted public pilot | Notebook `creator-learning-os/docs/qa/2026-10-06-public-activation.md` records real allowed-test-user Google identity + separate YouTube consent, AOP/TLS trust boundary; Oct 8 bounded deterministic report, email and recovery check. `https://creator.writingdeveloper.blog/demo` returns 200 anonymously | Clarified Testing-only external pilot, free public no-login demo, not general paid sign-up nor model-powered generation; date Oct 8 | +| **Creator OS engineering** | Two separate entries looked like two products | Same product, different engineering/release case, with operator gated public pilot | Named `Creator OS — Release Engineering`, explains shared product lineage; not two SaaS companies | +| **Soursea** | Local personal sourcing tool could be mistaken for company product | `soursea-site/docs/FACTS.md` explicitly excludes the personal Electron sourcing tool from Soursea LLC customer catalog | Renamed `Soursea — Personal Sourcing Workspace` and labeled private/noncommercial product | +| **Studio** | Highly technical listing has no updatedAt and no four-field case study; strong model-count wording relies on older tests | Notebook `studios` monorepo README, latest `studios-wt-orbit` worktree and QA backlog document ongoing changes, including test isolation lessons; not yet canonical merged proof | **Do not claim new experimental branch models or results are released.** Stage a dated architecture/QA case study only after checking canonical main, ComfyUI runtime, current 5 modalities and redacted screenshots | +| **MingleStage** | Entirely absent from 48-entry catalogue despite Soursea LLC early access | Live home and `/app/` HTTP 200; company fact ledger calls it Early Access. Prior FOLIO-33 decision expressly deferred a launched case study pending stable anonymous/guest room and media/cross-client proof | Keep **FOLIO-33 gate**; if passes, add `building / early access` with synthetic screenshots, not `launched` or user/revenue metrics | +| **STRATA / RailForge / Music-MV Studio** | Many current uncommitted or research-only efforts | MAIN git `project-strata` 2026-10-06, `railforge` 2026-09-29; Notebook music-mv README. Runtime/vertical slice and H3 binding remain acceptance gates | Keep `building/private`. Update after real accepted gameplay/workflow demonstration, not based on file counts | +| **Other recent code** | Personal OpenMontage/Wan meme, Roblox avatar and private media experiments | MAIN local commits and experiments exist | Do not indiscriminately promote. Publish only with permission, licensing/privacy review and a reproducible technical claim | + +## 4. Editorial and information architecture + +- There are **48 entries**, of which **36 have no verified `updatedAt`**. This is an evidence gap, not license to fabricate dates. Date only when a dated immutable source or production acceptance exists. +- Retain **five curated selected-work items** to communicate different skills, rather than placing 48 equally above the fold. Strongest evidence-led hooks: DevDeck user-visible state repair; citefirst retrieval denominator and caveat; BRAIN//VAT interactive public verification; Creator OS release gate; Studio after fresh artifact QA. +- If introducing a new post, choose one carefully documented real failure or release decision rather than another AI-framework roundup. Current candidate includes September 22's bilingual agent-ownership blog post; public last highlighted post had been August 27. +- Audit the first-screen Hire conversion path after the final deployment. The public site currently breaks the résumé, which overwhelms improvements to aesthetics and metadata. +- Cross-link `soursea.io` and the individual portfolio with explicit **personal vs LLC** distinction. Do not describe the personal sourcing desktop as a live Soursea product. +- Do not claim model-generated work is a customer outcome. Label deterministic rules and simulated NPCs correctly; distinguish skipped tests, historical QA, local preview and production. +- Public text remains bilingual; invalid localized page links, dead images, 404 PDF CTAs and accessibility regressions should be caught by Playwright. + +## 5. Proposed follow-up order and Plane mapping + +1. **P0 · FOLIO-45**: resolve/upstream-patch the actual `braces` transitive vulnerability, prove clean Linux/Windows CI. A time-limited explicit security-risk decision is a separate authorization, not assumed. +2. **P0 · FOLIO-36**: owner reviews résumé snapshots and claims; merge #33/#34 through their required gates, deploy to Vercel, verify public HTTP 200/application/pdf/SHA for all three PDF versions plus contact CTA/mobile/EN-KO/rollback. +3. **P1 · FOLIO-51 (new)**: retain repeatable public-link health checks, investigate Kindling origin timeouts and after-release public résumé health. Do not conflate private GitHub 404s with public broken links. +4. **P1 · FOLIO-46**: source-backed project curation, including Oct 13 BRAIN//VAT NPC and Creator OS restricted pilot; next Studio current canonical verification and proof-led case study. +5. **P1 · FOLIO-33**: evaluate MingleStage as a *labelled early-access* case only once real anonymous/guest, media and mobile/cross-client gates pass. Historical pilot notes are not unconditional launch proof. +6. **P2 · FOLIO-48**: capture same-host Lighthouse and performance budgets after preview/production alignment; make projects catalogue lighter and assess proof-card scroll depth. Prior 509 KB public-vs-485 KB local decoded HTML is a noncontrolled comparison, not a field LCP win. +7. **P2 · FOLIO-35/49**: revisit specific target job/contract descriptions and portfolio's real conversion evidence in December. Do not publish vague AI productivity percentages. + +No unrelated projects were restarted, the 4080 original WIP checkout remains preserved, and Notebook worktrees were inspected read-only. A source preview build is not live production, and production was not modified by this audit. +## 2026-10-13 same-day verification addendum + +### Automated candidate QA + +- Next.js 16.4.0 candidate: TypeScript, ESLint, Prettier, 244/244 unit tests across 26 files, evidence checks, all versioned résumé PDF bytes, and 11/11 bilingual posts PASS. Production build PASS with 148/148 static-generation tasks. +- Production-mode preview on localhost: Chromium EN/KO x 1440/390px x dark/light 8/8 PASS, DevDeck historical failure case both locales, BRAIN//VAT NPC screenshot/live link plus its explicit no-trained-policy disclaimer, Creator OS Testing-only restriction and its anonymous public demo in both locales, all 3 local PDFs with correct SHA-256 and MIME. +- The new per-locale NPC/Creator browser probe initially shared a page between locales and produced a stale-language assertion. A fresh Chromium context per locale resolved that QA harness issue without weakening the assertions. Both separately loaded pages contained the correct training limitation. +- Public-link audit against local preview: 56 reachable / 0 broken / 0 uncertain. This is not a claim that public production résumé PDFs are fixed. Production and preview JSON reports are retained separately under ignored .qa/portfolio/. + +### Intermittent Kindling response + +- Early audit attempts timed out (11-second Python/Node, 22-second GET and 15-second HEAD), but a later independent check of the same external domain returned HTTP 200 in 5.7 seconds. This is intermittent accessibility from the review host, not proof of permanent shutdown. Recheck from a second network before altering the published link. + +### Release guard + +- The unresolved production braces advisory remains unapproved. Changes are confined to the candidate draft PR, not public production. The current external pilot is Testing-only and the BRAIN//VAT NPC is deterministic, not trained or LLM-driven. diff --git a/docs/reviews/minglestage-case-study-2026-09-22.md b/docs/reviews/minglestage-case-study-2026-09-22.md new file mode 100644 index 0000000..16da5ca --- /dev/null +++ b/docs/reviews/minglestage-case-study-2026-09-22.md @@ -0,0 +1,11 @@ +# MingleStage case-study decision — 2026-09-22 + +Source reviewed: crowdstage README on 4080. README file revision: 2194ec131083ce9a12ab362579a52255278e5ba1. Repository HEAD during the read: c2422d675076a93b29d3229497e8deb9830e9746. Active development may move these references. + +**Decision: do not promote this to a publicly launched portfolio case study in this pass.** This preserves the distinction between engineering work and public readiness. + +The README reports an invitation-only pilot, with runtime source 613abf5bfdb5a701f9901dcecfd7fc02b8dd3881. The new direction is open signup, password-protected private rooms and restricted guests; these have separate source and production gates. Staged signup implementation is not proof that the public journey is available. + +Promotion requires a stable public entry flow, a version-matched deployment/readiness record, synthetic-account screenshots, media/cross-client evidence, and explicit physical-device/group/Safari limitations. The portfolio review did not operate production accounts, create rooms, change MingleStage code, or invent adoption/performance numbers. + +Keep the existing AI-focused selected work. Revisit MingleStage when its own public-entry and release gates pass. This decision resolves the portfolio selection question without claiming the underlying product is finished. diff --git a/docs/reviews/portfolio-qa-2026-09-22.md b/docs/reviews/portfolio-qa-2026-09-22.md new file mode 100644 index 0000000..da69edd --- /dev/null +++ b/docs/reviews/portfolio-qa-2026-09-22.md @@ -0,0 +1,66 @@ +# Portfolio implementation and QA record — 2026-09-22 (Pacific Time) + +## Result + +Implementation and local automated verification completed. **Not committed, pushed or deployed.** GitHub CI was configured and its YAML parsed; no remote CI run was claimed. Human visual approval and physical-device acceptance remain pending. + +Working branch: `fix/positioning-alignment-2026-09`. +Local production-preview source fingerprint: `6d5c4a9c88c92bdc30f58d50a0bf410f6d498d3c028b78df5f3f6e7feeb1fe8c`. +Browser report recorded at `2026-09-23T05:53:35.646Z` (September 22 in Los Angeles). + +## Verified changes + +- Shared AI-first skill grouping for standard pages and the 3D SkillsSection. +- Corrected résumé CTA background token and mobile project-card badge wrapping. +- Home links to résumé/hire, Studio and every selected project's case study, including private studios. +- Local/preview telemetry no longer requests Vercel-only analytics endpoints or sends production analytics. +- Reviewed evidence manifest pins source commit, normalized text SHA-256, date, numeric measurements and limitations. +- Read-only evidence collection produces ignored review candidates; it never rewrites public content. +- Hire exposes source-linked evidence and the English résumé PDF snapshot date (2026-09-14). +- Persisted browser QA and CI artifact retention replace temporary QA scripts. + +## Corrected evidence claims + +DevDeck's 1,257 passing tests belonged to historical 1.37.7 validation. The published evidence panel now references the dated 1.37.8 maintenance-candidate record: 1,289 passed, one skip. This is not a newly run DevDeck suite, proof of public release or physical-network acceptance. + +citefirst's working README contained 56-question results, but these changes were uncommitted. The collector caught the mismatch against the pinned Git source. Published figures remain the committed 46-question record: recall@5 0.978, MRR@10 0.834. The 56-question figures (0.982 / 0.785) appear only in the private candidate report. Citation-ID validity is not answer correctness. + +## Checks + +| Check | Result | +| --- | --- | +| TypeScript, ESLint, Prettier | Pass | +| Unit/regression suite | 23 files; 223 passed | +| Evidence and résumé-byte checks | Pass; 2 pinned sources verified | +| Evidence candidates | 1 uncommitted-worktree difference; no automatic publication | +| Bilingual posts | 11/11 pairs | +| Production build | 138/138 generation tasks complete | +| Chromium EN/KO × 1440/390px × dark/light | All 8 combinations pass | +| Console/page/network assertions | Pass for matrix routes; no unexpected errors | +| Résumé CTA contrast | Dark 15.28:1; light 5.47:1 | +| New article and Creator OS routes | HTTP 200 in both languages | +| Served PDF integrity | 3/3 application/pdf responses with expected SHA-256 | +| EN and KO navigation suites | Both pass | +| npm audit | 0 known reported vulnerabilities at check time | +| git diff --check | Pass | + +Machine report and eight full-page captures are under `.qa/portfolio/`. Earlier failed origin checks are retained separately. Capturing screenshots and checking geometry/contrast do not amount to human visual acceptance. + +## Preview origin finding + +The Windows Next.js 16.3.3 preview needed consistent `localhost` binding and request origin. An explicit 127.0.0.1 bind produced an unprefixed-locale rewrite loop. An older preview also existed on port 3100, so isolated final verification used `localhost:3190` and a newly started candidate process. Do not treat the earlier mixed-origin result as a production defect or passing candidate evidence. + +## Plane read-back + +- FOLIO-33: Done — selection review complete; defer public MingleStage promotion until its own release gates pass. +- FOLIO-34: QA — implementation and automated QA evidence; not public deployment. +- FOLIO-35: Todo, target 2026-12-22 — first review recorded; next review queued, no automation scheduled. +- FOLIO-36: Todo — newly separated public deployment and live résumé/evidence QA. + +The QA column's display-order change was not applied. Existing statuses remain in use. + +## Remaining release work + +Read-only public check: `/en/hire` returned 200 without the new evidence section; `/resume/Sihyeong-Lee-Resume.pdf` returned 404. Review/commit/push/deployment and same-release public URL verification are therefore still needed under FOLIO-36. + +The authoritative résumé originals in notebook/hangar were not edited. Their numeric wording needs a separate editorial refresh before regenerating public PDFs. No customer adoption, revenue, production deployment or human approval was invented. diff --git a/messages/en.json b/messages/en.json index 98f2b22..e0a5160 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,7 +1,7 @@ { "metadata": { "title": "WritingDeveloper", - "description": "Build logs and engineering notes from Sihyeong Lee — an AI-native product engineer shipping developer tools, applied AI systems, and production full-stack products." + "description": "Engineering notes and source-backed case studies on browser automation, evaluated retrieval, agent tooling, and production release decisions by Sihyeong Lee." }, "nav": { "home": "Home", @@ -16,15 +16,15 @@ "hero": { "greeting": "Hello", "name": "Sihyeong Lee", - "role": "AI-Native Product Engineer", - "description": "I design, ship, and operate AI-native products and developer infrastructure." + "role": "AI-Native Product & Agent Systems Engineer", + "description": "AI-assisted code is a starting point. I define acceptance criteria, test real workflows, investigate failure modes, and distinguish successful builds from safe releases." }, "latestPosts": "Latest Posts", "viewAll": "View All", "featuredProjects": "Projects", "noPosts": "No posts yet.", "explore": "Explore my world", - "metaDescription": "Sihyeong Lee — AI-native product engineer in Los Angeles. I build and operate developer tools, RAG systems, local-AI/GPU infrastructure, and production full-stack products." + "metaDescription": "Sihyeong Lee — AI-native product and agent-systems engineer in Los Angeles. Source-backed automation, measured retrieval, recovery design, and accountable software delivery." }, "blog": { "title": "Blog", @@ -54,9 +54,9 @@ }, "projects": { "title": "Projects", - "description": "Things I'm building", + "description": "A documented ledger of released software, active prototypes, and unfinished experiments", "metaTitle": "{count} Projects — Web, Games, Dev Tools", - "metaDescription": "{count} projects built solo by Sihyeong Lee — SaaS, developer tools, games and AI experiments, each with a live demo or store link.", + "metaDescription": "{count} documented software projects, from public products to private prototypes. Live demos, source links and measured claims are shown only where available.", "viewProject": "View Project", "viewShowcase": "See the work", "viewCode": "View Code", @@ -75,9 +75,9 @@ "archived": "Archived" }, "selectedWork": "Selected Work", - "selectedWorkDescription": "Five projects selected to show agent tooling, AI infrastructure, measured RAG, production full-stack work, and desktop systems engineering.", + "selectedWorkDescription": "Engineering decisions and verifiable evidence across developer tooling, measured retrieval, recovery, and production release gates.", "fullLedger": "Full Ledger", - "fullLedgerDescription": "The rest of the record, including work that is still being built.", + "fullLedgerDescription": "Research, prototypes, and private work are included. A build count is not a release or customer count.", "caseStudy": { "heading": "How it was solved", "problem": "Problem", @@ -112,33 +112,33 @@ }, "about": { "title": "About", - "description": "AI-native product engineering, from architecture to production operations", - "metaTitle": "About — AI-Native Product Engineer & Senior Full-Stack", - "metaDescription": "Sihyeong Lee — AI-native product engineer and former CTO in Los Angeles. Developer tooling, applied AI, full-stack products, local GPU infrastructure, and the decisions behind them.", - "body": "I am an AI-native product engineer and former CTO who owns the full path from architecture and implementation through testing, deployment, and production operations. I use coding agents heavily, but the engineering decisions stay explicit: constraints, acceptance criteria, failure analysis, and what ships. Recent work spans developer tooling such as DevDeck, measured RAG in citefirst, civic-tech systems such as RentRights, and a self-hosted MCP/GPU generation platform running across my own infrastructure.", + "description": "Engineering judgment across architecture, evaluation, failure recovery, and release decisions", + "metaTitle": "About — AI-Native Product & Agent Systems Engineer", + "metaDescription": "Sihyeong Lee — AI-native product and agent-systems engineer and former CTO in Los Angeles. Agent tooling, MCP, evals, applied AI, full-stack systems, and production infrastructure.", + "body": "I am an AI-native product engineer and former CTO who builds the systems around modern models, not just prompts: agent workflows, MCP/tool integrations, evaluation harnesses, retrieval systems, developer tooling, and the full-stack infrastructure that carries them into production. I use coding agents heavily as execution leverage while retaining ownership of architecture, acceptance criteria, code review, testing, debugging, deployment, and release decisions. Recent work includes DevDeck, a command center for coding agents; studios, a multi-modal MCP/GPU platform with persisted verification; citefirst, a measured RAG system; and production SaaS work with explicit safety and release gates.", "skills": "Tech Stack", "timeline": "Timeline" }, "hire": { "title": "Work with me", - "description": "Senior product and full-stack engineer for browser automation, AI integration, and production release work — open to full-time roles and focused projects.", + "description": "AI is part of the toolchain, not the result. I build browser automations, production integrations, and release-ready systems with verifiable acceptance criteria.", "metaTitle": "Sihyeong Lee — Browser Automation, AI Integration & Senior Full-Stack", - "metaDescription": "Los Angeles senior software engineer for Playwright/browser automation, AI integration, production Next.js/PostgreSQL systems, and release QA. {shipped} shipped products with a public demo or store listing.", - "badgeRemote": "Los Angeles · Fully remote · US permanent resident", + "metaDescription": "Los Angeles senior software engineer for browser automation, testable AI integration, and production release QA. {shipped} projects with a public demo or store listing; verification scope is explicit.", + "badgeRemote": "Los Angeles · Remote-first · LA hybrid/on-site · US permanent resident", "badgeNow": "Available now", "fulltime": { "heading": "Full-time", "rolesLabel": "Roles", "roles": "Senior Software Engineer · AI Integration / Applied AI Engineer · Senior Full-Stack / Automation Engineer", "modeLabel": "Working style", - "mode": "Fully remote (US, Pacific Time) · US permanent resident — no sponsorship required", + "mode": "Remote-first (US, Pacific Time) · open to LA hybrid/on-site · US permanent resident — no sponsorship required", "bringLabel": "What I bring", - "bring": "Nearly five years owning engineering as a CTO across two companies, plus hands-on work in Playwright automation, AI-enabled products, PostgreSQL/Docker production systems, CI/release gates, RAG, and local GPU infrastructure." + "bring": "Nearly five years owning engineering as a CTO across two companies. I define technical constraints, review AI-assisted implementations, test failure and recovery paths, and separate working code from launch approval." }, "contract": { "heading": "Focused engineering projects", "workLabel": "Problems I solve", - "work": "Playwright/browser automation · AI integration into existing products · Next.js/PostgreSQL release & QA rescue", + "work": "Resilient Playwright/browser workflows · AI integration with measurable evals and human review · production release gates, recovery, and QA", "modeLabel": "Shape", "mode": "Remote-first. Small paid diagnostics and implementation sprints measured in days, with clear acceptance criteria and evidence.", "startLabel": "How to start", @@ -146,13 +146,58 @@ }, "evidence": { "heading": "Evidence", - "shipped": "products with a public demo or store listing", + "shipped": "projects with a public demo or store link (not necessarily a commercial launch)", "playStore": "apps on Google Play", "total": "builds in the ledger", "caseHeading": "Relevant case studies" }, "stack": "Tech Stack", - "contactHeading": "Get in touch" + "contactHeading": "Get in touch", + "resume": { + "heading": "Résumé", + "primary": "View résumé", + "specializedLabel": "Specialized versions", + "devex": "Developer Experience / Agent Tooling", + "fde": "Forward Deployed / Production Integration" + }, + "experience": { + "heading": "Experience snapshot", + "soursea": { + "title": "Soursea LLC · Founder & Software Engineer · 2025–Present", + "body": "Agent systems, MCP servers, eval harnesses, full-stack products, and self-hosted GPU/cloud infrastructure." + }, + "itembox": { + "title": "ITEMBOX / Bitnara · CTO · 2024–2025", + "body": "Led nine engineers across Korea and Vietnam while owning a 20-module NestJS platform, integrations, billing, AI services, and AWS operations." + }, + "geomec": { + "title": "GEOMEC · CTO · 2021–2024", + "body": "Sole engineer for desktop software, Linux/network infrastructure, virtualization, access control, and backups." + }, + "casefarms": { + "title": "Case Farms · Quality Assurance · 2023–2024", + "body": "U.S. manufacturing-floor role; moved from production into quality assurance after five months." + } + }, + "approach": { + "heading": "Beyond the AI-generated demo", + "intro": "AI can accelerate implementation. The differentiator is what survives a failure, a review, and the decision to deploy. Here is how I frame the engineering work, with actual examples below.", + "decision": { + "title": "Frame the real problem", + "body": "Start with workflows, constraints, state ownership, and failure cases. A model or tool is an implementation choice, not a requirements document.", + "link": "DevDeck: state & recovery" + }, + "verification": { + "title": "Verify the outcome", + "body": "Use dated, source-linked test runs and explicit denominators. Passing retrieval metrics are not the same as correct generated answers.", + "link": "citefirst: measured retrieval" + }, + "release": { + "title": "Ship — or deliberately block", + "body": "A successful build does not clear external OAuth, backups, operator decisions, or production rollout. Keep unverified gates visibly open.", + "link": "Creator OS: release gates" + } + } }, "studio": { "title": "The Studio", diff --git a/messages/ko.json b/messages/ko.json index d7d3812..02612d9 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -1,7 +1,7 @@ { "metadata": { "title": "WritingDeveloper", - "description": "AI 네이티브 제품 엔지니어 이시형의 개발 기록. 개발자 도구, Applied AI, 프로덕션 풀스택 제품과 실제 운영 과정을 다룹니다." + "description": "브라우저 자동화, 검색 평가, 에이전트 도구, 프로덕션 출시 판단을 원본 근거와 함께 기록하는 이시형의 엔지니어링 포트폴리오." }, "nav": { "home": "홈", @@ -16,15 +16,15 @@ "hero": { "greeting": "안녕하세요", "name": "이시형", - "role": "AI 네이티브 제품 엔지니어", - "description": "AI 네이티브 제품과 개발 인프라를 설계하고 배포·운영합니다." + "role": "AI 네이티브 제품 · 에이전트 시스템 엔지니어", + "description": "AI가 코드를 만드는 것은 출발점입니다. 완료 기준을 정의하고, 실제 사용 흐름과 실패 상황을 검증하며, 빌드 성공과 안전한 출시를 구분합니다." }, "latestPosts": "최근 글", "viewAll": "모두 보기", "featuredProjects": "프로젝트", "noPosts": "글이 없습니다.", "explore": "내 세계를 탐험하기", - "metaDescription": "로스앤젤레스의 AI 네이티브 제품 엔지니어 이시형. 개발자 도구, RAG 시스템, 로컬 AI/GPU 인프라, 프로덕션 풀스택 제품을 직접 구축·운영합니다." + "metaDescription": "로스앤젤레스의 AI 네이티브 제품·에이전트 시스템 엔지니어 이시형. 검증 가능한 자동화, 측정된 검색 품질, 장애 복구와 출시 책임을 사례로 설명합니다." }, "blog": { "title": "블로그", @@ -54,9 +54,9 @@ }, "projects": { "title": "프로젝트", - "description": "만들고 있는 것들", + "description": "출시한 소프트웨어부터 개발 중인 시제품·비공개 실험까지 기록합니다", "metaTitle": "프로젝트 {count}개 · 웹·앱·게임·개발자 도구", - "metaDescription": "이시형이 직접 만든 프로젝트 {count}개. SaaS와 개발자 도구, 게임, AI 실험까지 라이브 데모·스토어 링크와 함께 정리했습니다.", + "metaDescription": "공개 제품, 연구용 프로토타입, 비공개 실험을 포함한 소프트웨어 프로젝트 {count}개의 기록. 실제 공개 가능한 데모·소스와 검증 근거만 연결합니다.", "viewProject": "프로젝트 보기", "viewShowcase": "결과물 보기", "viewCode": "코드 보기", @@ -75,9 +75,9 @@ "archived": "보관됨" }, "selectedWork": "대표 작업", - "selectedWorkDescription": "에이전트 도구, AI 인프라, 측정 가능한 RAG, 프로덕션 풀스택, 데스크톱 시스템 엔지니어링을 보여주는 대표 프로젝트 5개입니다.", + "selectedWorkDescription": "개발자 도구, 검색 품질 평가, 장애 복구와 출시 게이트에 대한 엔지니어링 결정 및 검증 근거를 소개합니다.", "fullLedger": "전체 원장", - "fullLedgerDescription": "진행 중인 것까지 포함한 나머지 프로젝트 기록입니다.", + "fullLedgerDescription": "연구·시제품·비공개 작업도 포함됩니다. 제작 수가 출시 수나 고객 수를 의미하지는 않습니다.", "caseStudy": { "heading": "어떻게 풀었는가", "problem": "문제", @@ -112,33 +112,33 @@ }, "about": { "title": "소개", - "description": "아키텍처부터 프로덕션 운영까지 이어지는 AI 네이티브 제품 엔지니어링", - "metaTitle": "이시형 · AI 네이티브 제품 엔지니어 & 시니어 풀스택", - "metaDescription": "로스앤젤레스의 AI 네이티브 제품 엔지니어이자 전 CTO 이시형. 개발자 도구, Applied AI, 풀스택 제품, 로컬 GPU 인프라와 그 안의 엔지니어링 판단을 기록합니다.", - "body": "AI 네이티브 제품 엔지니어이자 전 CTO로서 아키텍처와 구현부터 테스트, 배포, 프로덕션 운영까지 전체 경로를 직접 책임집니다. Claude Code와 Codex 같은 코딩 에이전트를 적극적으로 사용하지만, 제약조건·완료 기준·실패 분석·출시 판단은 명시적으로 소유합니다. 최근에는 DevDeck 같은 개발자 도구, citefirst의 측정형 RAG, RentRights 같은 시빅테크, 직접 운영하는 MCP/GPU 생성 플랫폼을 만들고 운영하고 있습니다.", + "description": "아키텍처·평가·장애 복구·출시 결정에서 드러나는 엔지니어링 판단", + "metaTitle": "소개 — AI 네이티브 제품 · 에이전트 시스템 엔지니어", + "metaDescription": "로스앤젤레스의 AI 네이티브 제품·에이전트 시스템 엔지니어이자 전 CTO 이시형. 에이전트 도구, MCP, eval, Applied AI, 풀스택 시스템, 프로덕션 인프라.", + "body": "저는 최신 모델 자체보다 그 모델이 실제 일을 하게 만드는 주변 시스템까지 만드는 AI 네이티브 제품 엔지니어이자 전 CTO입니다. 에이전트 워크플로, MCP·툴 연동, 평가 하니스, 검색 시스템, 개발자 도구, 그리고 이를 프로덕션까지 운반하는 풀스택 인프라를 직접 다룹니다. 코딩 에이전트를 적극적으로 실행 레버리지로 사용하지만 아키텍처, 수용 기준, 코드 리뷰, 테스트, 디버깅, 배포, 릴리스 결정은 제가 책임집니다.", "skills": "기술 스택", "timeline": "타임라인" }, "hire": { "title": "함께 일하기", - "description": "브라우저 자동화·기존 제품 AI 통합·프로덕션 릴리스 문제를 해결하는 시니어 제품/풀스택 엔지니어입니다. 정규직과 집중형 프로젝트 모두 가능합니다.", + "description": "AI 사용 자체가 성과는 아닙니다. 검증 가능한 완료 기준을 갖춘 브라우저 자동화, 프로덕션 연동, 출시 품질 문제를 해결합니다.", "metaTitle": "이시형 — 브라우저 자동화 · AI 통합 · 시니어 풀스택", - "metaDescription": "로스앤젤레스의 시니어 소프트웨어 엔지니어. Playwright/브라우저 자동화, 기존 제품 AI 통합, Next.js/PostgreSQL 프로덕션 시스템과 릴리스 QA를 수행합니다. 공개 데모·스토어가 있는 제품 {shipped}건.", - "badgeRemote": "로스앤젤레스 · 전면 원격 · 미국 영주권자", + "metaDescription": "로스앤젤레스의 시니어 소프트웨어 엔지니어. 브라우저 자동화, 검증 가능한 AI 통합, 프로덕션 출시 QA를 다룹니다. 공개 데모·스토어 링크가 있는 프로젝트 {shipped}건을 구분해 소개합니다.", + "badgeRemote": "로스앤젤레스 · 원격 우선 · LA 하이브리드/출근 가능 · 미국 영주권자", "badgeNow": "지금 바로 시작 가능", "fulltime": { "heading": "정규직", "rolesLabel": "찾는 역할", "roles": "시니어 소프트웨어 엔지니어 · AI 통합 / Applied AI 엔지니어 · 시니어 풀스택 / 자동화 엔지니어", "modeLabel": "근무 형태", - "mode": "전면 원격(미국, 태평양 표준시) · 영주권자(취업 스폰서 불필요)", + "mode": "원격 우선(미국, 태평양 시간) · LA 하이브리드/출근 가능 · 영주권자(취업 스폰서 불필요)", "bringLabel": "가져오는 것", - "bring": "두 회사에서 5년 가까이 엔지니어링을 책임진 CTO 경험과 Playwright 자동화, AI 제품, PostgreSQL/Docker 프로덕션 시스템, CI/릴리스 게이트, RAG, 로컬 GPU 인프라를 직접 구축한 최근 경험." + "bring": "두 회사에서 5년 가까이 엔지니어링을 책임진 CTO 경험이 있습니다. 기술적 제약과 완료 기준을 정하고, AI 보조 구현물을 검토하며, 장애·복구를 검증하고 코드 완성과 실제 출시 승인을 구분합니다." }, "contract": { "heading": "집중형 엔지니어링 프로젝트", "workLabel": "해결하는 문제", - "work": "Playwright/브라우저 자동화 · 기존 제품 AI 통합 · Next.js/PostgreSQL 릴리스·QA 구조화", + "work": "장애에 강한 Playwright/브라우저 자동화 · 평가·사람 검수가 포함된 AI 통합 · 프로덕션 출시 게이트·복구·QA", "modeLabel": "규모 · 기간", "mode": "원격 우선. 완료 기준과 증거가 명확한 유료 진단·구현 스프린트를 일 단위로 진행합니다.", "startLabel": "시작하는 법", @@ -146,13 +146,58 @@ }, "evidence": { "heading": "증거", - "shipped": "공개 데모·스토어가 있는 제품", + "shipped": "공개 데모·스토어 링크가 있는 프로젝트(상용 출시와 별개)", "playStore": "Play Store 앱", "total": "총 빌드", "caseHeading": "관련 사례" }, "stack": "기술 스택", - "contactHeading": "연락" + "contactHeading": "연락", + "resume": { + "heading": "이력서", + "primary": "이력서 보기", + "specializedLabel": "직무별 버전", + "devex": "Developer Experience / Agent Tooling", + "fde": "Forward Deployed / Production Integration" + }, + "experience": { + "heading": "경력 요약", + "soursea": { + "title": "Soursea LLC · Founder & Software Engineer · 2025–현재", + "body": "에이전트 시스템, MCP 서버, eval 하니스, 풀스택 제품, 셀프호스트 GPU/클라우드 인프라." + }, + "itembox": { + "title": "ITEMBOX / Bitnara · CTO · 2024–2025", + "body": "한국·베트남 9인 개발팀을 이끌며 20모듈 NestJS 플랫폼, 외부 연동, 결제, AI 서비스, AWS 운영을 직접 책임." + }, + "geomec": { + "title": "GEOMEC · CTO · 2021–2024", + "body": "데스크톱 소프트웨어, Linux/네트워크 인프라, 가상화, 접근제어, 백업을 담당한 단독 엔지니어." + }, + "casefarms": { + "title": "Case Farms · Quality Assurance · 2023–2024", + "body": "미국 제조 현장 근무. 생산라인에서 시작해 5개월 뒤 품질보증으로 이동." + } + }, + "approach": { + "heading": "AI 데모를 넘어, 검증 가능한 소프트웨어", + "intro": "AI가 구현을 빠르게 해줘도 장애 대응과 배포 판단을 대신 책임지지는 않습니다. 실제 사례에 연결해 제가 문제를 정의하고 검증하는 방식을 설명합니다.", + "decision": { + "title": "문제와 실패 조건 정의", + "body": "실제 작업 흐름, 제약, 상태의 소유권과 실패 조건을 먼저 정합니다. 모델·도구 선택은 요구사항의 대체물이 아닙니다.", + "link": "DevDeck: 상태·복구 설계" + }, + "verification": { + "title": "결과를 측정하고 검증", + "body": "실행 날짜·원본 소스·평가 문항 수가 고정된 증거를 사용합니다. 검색 지표 통과가 생성 답변의 정확성을 보장하지는 않습니다.", + "link": "citefirst: 검색 평가" + }, + "release": { + "title": "출시하거나, 근거가 없으면 차단", + "body": "빌드 성공만으로 OAuth·백업·운영 판단·실서비스 배포가 검증되지는 않습니다. 미완료 게이트를 명확하게 남깁니다.", + "link": "Creator OS: 출시 게이트" + } + } }, "studio": { "title": "스튜디오", diff --git a/package.json b/package.json index 89ae4d2..63402c2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,12 @@ "verify:hero": "tsx scripts/verify-hero.ts", "capture:screenshots": "tsx scripts/capture-screenshots.ts", "fetch:fonts": "tsx scripts/fetch-pretendard-subset.ts", - "qa:navigation": "tsx scripts/qa-navigation.ts" + "qa:navigation": "tsx scripts/qa-navigation.ts", + "evidence:check": "tsx scripts/evidence.mts", + "evidence:collect": "tsx scripts/evidence.mts --collect", + "evidence:verify": "tsx scripts/evidence.mts --verify-local", + "qa:portfolio": "tsx scripts/qa-portfolio.mts", + "audit:public-links": "tsx scripts/audit-public-links.mts" }, "dependencies": { "@keystatic/core": "^0.5.48", diff --git a/public/images/projects/brain-vat-npc-production.webp b/public/images/projects/brain-vat-npc-production.webp new file mode 100644 index 0000000..af52769 Binary files /dev/null and b/public/images/projects/brain-vat-npc-production.webp differ diff --git a/public/images/projects/brain-vat-siege.webp b/public/images/projects/brain-vat-siege.webp new file mode 100644 index 0000000..3ea7ce3 Binary files /dev/null and b/public/images/projects/brain-vat-siege.webp differ diff --git a/public/images/projects/devdeck-sync-qa-after.webp b/public/images/projects/devdeck-sync-qa-after.webp new file mode 100644 index 0000000..f082ec7 Binary files /dev/null and b/public/images/projects/devdeck-sync-qa-after.webp differ diff --git a/public/images/projects/devdeck-sync-qa-before.webp b/public/images/projects/devdeck-sync-qa-before.webp new file mode 100644 index 0000000..951c8c1 Binary files /dev/null and b/public/images/projects/devdeck-sync-qa-before.webp differ diff --git a/public/resume/Sihyeong-Lee-Resume-DevEx.pdf b/public/resume/Sihyeong-Lee-Resume-DevEx.pdf new file mode 100644 index 0000000..aea15c5 Binary files /dev/null and b/public/resume/Sihyeong-Lee-Resume-DevEx.pdf differ diff --git a/public/resume/Sihyeong-Lee-Resume-FDE.pdf b/public/resume/Sihyeong-Lee-Resume-FDE.pdf new file mode 100644 index 0000000..efb608f Binary files /dev/null and b/public/resume/Sihyeong-Lee-Resume-FDE.pdf differ diff --git a/public/resume/Sihyeong-Lee-Resume.pdf b/public/resume/Sihyeong-Lee-Resume.pdf new file mode 100644 index 0000000..c1bf6ab Binary files /dev/null and b/public/resume/Sihyeong-Lee-Resume.pdf differ diff --git a/scripts/audit-public-links.mts b/scripts/audit-public-links.mts new file mode 100644 index 0000000..fe7b58d --- /dev/null +++ b/scripts/audit-public-links.mts @@ -0,0 +1,122 @@ +import fs from 'node:fs' +import path from 'node:path' +import projects from '../content/projects.json' + +interface Target { + kind: 'route' | 'demo' | 'public-source' + id: string + url: string +} +interface Result extends Target { + status: number | null + resolvedUrl?: string + error?: string + elapsedMs: number +} + +const origin = (process.env.PORTFOLIO_AUDIT_ORIGIN || 'https://writingdeveloper.blog').replace(/\/$/, '') +const includeAllProjects = process.argv.includes('--all-projects') +const internalRoutes = [ + '/', + '/en', + '/hire', + '/en/hire', + '/projects', + '/en/projects', + '/blog', + '/en/blog', + '/studio', + '/en/studio', + '/robots.txt', + '/sitemap.xml', + '/llms.txt', + '/resume/Sihyeong-Lee-Resume.pdf', + '/resume/Sihyeong-Lee-Resume-DevEx.pdf', + '/resume/Sihyeong-Lee-Resume-FDE.pdf', + '/projects/devdeck', + '/en/projects/devdeck', + '/projects/brain-vat-siege', + '/en/projects/brain-vat-siege', + '/projects/creator-learning-os', + '/en/projects/creator-learning-os', +] +const urls: Target[] = internalRoutes.map((id) => ({ kind: 'route', id, url: origin + id })) +if (includeAllProjects) { + for (const project of projects.projects) { + for (const lang of ['', '/en']) { + urls.push({ + kind: 'route', + id: lang + '/projects/' + project.slug, + url: origin + lang + '/projects/' + project.slug, + }) + } + } +} +for (const project of projects.projects) { + if (project.website) urls.push({ kind: 'demo', id: project.slug, url: project.website }) + // Private source URLs are never linked to the user. An unauthenticated 404 + // from a private GitHub repository is NOT evidence of a broken public CTA. + if (project.github && !project.private) urls.push({ kind: 'public-source', id: project.slug, url: project.github }) +} +const seen = new Set() +const targets = urls.filter((item) => { + if (seen.has(item.url)) return false + seen.add(item.url) + return true +}) + +async function check(item: Target): Promise { + const start = performance.now() + const result: Result = { ...item, status: null, elapsedMs: 0 } + try { + const response = await fetch(item.url, { + redirect: 'follow', + headers: { 'user-agent': 'Mozilla/5.0 (PortfolioLinkQA; review-only)' }, + signal: AbortSignal.timeout(11000), + }) + result.status = response.status + result.resolvedUrl = response.url + await response.body?.cancel() + } catch (error) { + result.error = error instanceof Error ? error.message : String(error) + } + result.elapsedMs = Math.round(performance.now() - start) + return result +} + +const results: Result[] = [] +let index = 0 +await Promise.all( + Array.from({ length: 8 }, async () => { + while (index < targets.length) { + const item = targets[index++] + results.push(await check(item)) + } + }) +) +const brokenInternal = results.filter((r) => r.kind === 'route' && r.status !== null && r.status >= 400) +const externalProblems = results.filter((r) => r.kind !== 'route' && r.status !== null && r.status >= 400) +const uncertain = results.filter((r) => r.status === null) +const summary = { + checkedAt: new Date().toISOString(), + origin, + source: 'candidate project ledger; read-only HTTP headers, not functional browser QA', + checked: results.length, + ok: results.filter((r) => r.status !== null && r.status < 400).length, + brokenInternal: brokenInternal.length, + externalProblems: externalProblems.length, + uncertain: uncertain.length, + results: results.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)), +} +const directory = path.resolve('.qa/portfolio') +fs.mkdirSync(directory, { recursive: true }) +const file = path.join(directory, 'public-link-audit-latest.json') +fs.writeFileSync(file, JSON.stringify(summary, null, 2) + '\n') +console.log( + `Checked ${summary.checked} URLs: ${summary.ok} reachable, ${summary.brokenInternal} broken internal, ${summary.externalProblems} external HTTP errors, ${summary.uncertain} inconclusive` +) +for (const row of [...brokenInternal, ...externalProblems, ...uncertain]) { + console.log(`[${row.status ?? 'unknown'}] ${row.kind} ${row.id} ${row.url} ${row.error || ''}`) +} +console.log('Evidence:', file) +if (brokenInternal.length) process.exitCode = 1 diff --git a/scripts/evidence.mts b/scripts/evidence.mts new file mode 100644 index 0000000..5fa8a89 --- /dev/null +++ b/scripts/evidence.mts @@ -0,0 +1,106 @@ +import fs from 'node:fs' +import path from 'node:path' +import { createHash } from 'node:crypto' +import { execFileSync } from 'node:child_process' +import manifest from '../content/evidence.json' +import resume from '../content/resume-manifest.json' +import { EVIDENCE_SOURCES, evidenceProblems, evidenceValue, extractMeasurements } from '../src/lib/evidence-policy' + +const sha = (text: string | Buffer) => createHash('sha256').update(text).digest('hex') +const args = new Set(process.argv.slice(2)) +const allowed = new Set(['--collect', '--verify-local', '--require-current-review']) +for (const arg of args) if (!allowed.has(arg)) throw new Error('Unsupported evidence option: ' + arg) +const errors = evidenceProblems(manifest) +const expectedFiles = new Set([ + 'Sihyeong-Lee-Resume.pdf', + 'Sihyeong-Lee-Resume-DevEx.pdf', + 'Sihyeong-Lee-Resume-FDE.pdf', +]) +for (const entry of resume.files) { + if (!expectedFiles.delete(entry.file)) { + errors.push('Unknown/duplicate résumé file') + continue + } + const file = fs.readFileSync(path.join('public/resume', entry.file)) + if (file.subarray(0, 5).toString() !== '%PDF-' || sha(file) !== entry.sha256) + errors.push('Résumé bytes changed: ' + entry.file) +} +if (expectedFiles.size) errors.push('Missing résumé manifest entries') +const reviewDue = new Date().toISOString().slice(0, 10) > manifest.reviewAfter +if (reviewDue && args.has('--require-current-review')) errors.push('Evidence review overdue: ' + manifest.reviewAfter) +if (errors.length) throw new Error(errors.join('\n')) + +if (args.has('--collect') || args.has('--verify-local')) { + const sourceRoot = path.resolve(process.env.PORTFOLIO_SOURCE_ROOT ?? '..') + const candidates = [] + for (const record of manifest.records) { + const spec = EVIDENCE_SOURCES[record.id] + const cwd = path.resolve(sourceRoot, spec.localDirectory) + const git = (a: string[]) => + execFileSync('git', a, { cwd, encoding: 'utf8', timeout: 15_000, maxBuffer: 2_000_000 }) + .replace(/\r\n/g, '\n') + .trimEnd() + // git show reads the committed blob, never a worktree file, secret or arbitrary command. + const pinned = execFileSync('git', ['show', record.source.commit + ':' + spec.path], { + cwd, + encoding: 'utf8', + timeout: 15_000, + maxBuffer: 2_000_000, + }).replace(/\r\n/g, '\n') + if (sha(pinned) !== record.source.sha256) throw new Error('Pinned source digest mismatch: ' + record.id) + const parsed = extractMeasurements(record.id, pinned) + if (JSON.stringify(parsed) !== JSON.stringify(record.measurements)) + throw new Error('Measurement/source mismatch: ' + record.id) + const commit = git(['log', '-1', '--format=%H', '--', spec.path]) + if (!/^[a-f0-9]{40}$/.test(commit)) throw new Error('Source revision missing') + const text = execFileSync('git', ['show', commit + ':' + spec.path], { + cwd, + encoding: 'utf8', + timeout: 15_000, + maxBuffer: 2_000_000, + }).replace(/\r\n/g, '\n') + const measurements = extractMeasurements(record.id, text) + const candidate = { + id: record.id, + source: { repository: spec.repository, path: spec.path, commit, sha256: sha(text) }, + measurements, + changed: + commit !== record.source.commit || + sha(text) !== record.source.sha256 || + JSON.stringify(measurements) !== JSON.stringify(record.measurements), + requiresReview: true, + } + const worktreeText = fs.readFileSync(path.join(cwd, spec.path), 'utf8').replace(/\r\n/g, '\n') + const worktreeDiffers = sha(worktreeText) !== sha(text) + let worktreeMeasurements: Record | null = null + if (worktreeDiffers) { + try { + worktreeMeasurements = extractMeasurements(record.id, worktreeText) + } catch { + /* report changed bytes, do not guess */ + } + } + candidates.push({ ...candidate, worktreeDiffers, worktreeMeasurements, worktreePublished: false }) + console.log('SOURCE VERIFIED', record.id, evidenceValue(record)) + } + if (args.has('--collect')) { + fs.mkdirSync('.qa', { recursive: true }) + fs.writeFileSync( + '.qa/evidence-candidates.json', + JSON.stringify({ collectedAt: new Date().toISOString(), publicContentModified: false, candidates }, null, 2) + + '\n' + ) + console.log( + 'COLLECTED', + candidates.length, + 'candidates; changed:', + candidates.filter((c) => c.changed || c.worktreeDiffers).length, + '; public content unchanged' + ) + } +} +console.log( + 'PASS evidence structure, scope, résumé bytes; next review:', + manifest.reviewAfter, + reviewDue ? '(DUE)' : '' +) diff --git a/scripts/qa-navigation.ts b/scripts/qa-navigation.ts index d44f0f1..68968c9 100644 --- a/scripts/qa-navigation.ts +++ b/scripts/qa-navigation.ts @@ -2,7 +2,7 @@ import { chromium, type Page } from 'playwright' import enMessages from '../messages/en.json' import koMessages from '../messages/ko.json' -const BASE_URL = (process.env.QA_BASE_URL ?? 'http://127.0.0.1:3100').replace(/\/$/, '') +const BASE_URL = (process.env.QA_BASE_URL ?? 'http://localhost:3100').replace(/\/$/, '') const LOCALE = process.env.QA_LOCALE ?? 'en' const prefix = LOCALE === 'ko' ? '' : `/${LOCALE}` const navMessages = LOCALE === 'ko' ? koMessages.nav : enMessages.nav diff --git a/scripts/qa-portfolio.mts b/scripts/qa-portfolio.mts new file mode 100644 index 0000000..4d5f976 --- /dev/null +++ b/scripts/qa-portfolio.mts @@ -0,0 +1,244 @@ +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { chromium } from 'playwright' +import resumes from '../content/resume-manifest.json' + +const base = (process.env.QA_BASE_URL ?? 'http://localhost:3100').replace(/\/$/, '') +const output = path.resolve('.qa/portfolio') +fs.mkdirSync(output, { recursive: true }) +const sha = (data: Buffer | string) => createHash('sha256').update(data).digest('hex') +const sources = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8' }) + .split(/\r?\n/) + .filter((p) => /^(src|content|messages|scripts)\//.test(p) || /^(package.*json|next.config.ts)$/.test(p)) + .sort() +const sourceFingerprint = sha(sources.map((p) => p + ':' + sha(fs.readFileSync(p))).join('\n')) +const results: object[] = [] +const failures: string[] = [] +function assert(ok: unknown, message: string): asserts ok { + if (!ok) throw new Error(message) +} +function luminance(css: string) { + const rgb = css + .match(/[\d.]+/g) + ?.slice(0, 3) + .map(Number) + if (!rgb || rgb.length !== 3) throw new Error('Unrecognized computed color: ' + css) + const c = rgb.map((v) => { + const s = v / 255 + return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4 + }) + return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] +} +const browser = await chromium.launch({ headless: true }) +try { + for (const locale of ['en', 'ko']) + for (const width of [1440, 390]) + for (const theme of ['dark', 'light'] as const) { + const label = locale + '-' + width + '-' + theme + const ctx = await browser.newContext({ + viewport: { width, height: width === 390 ? 844 : 1000 }, + colorScheme: theme, + }) + await ctx.addInitScript((t) => localStorage.setItem('theme', t), theme) + const page = await ctx.newPage() + const errors: string[] = [] + const requests: string[] = [] + page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)) + page.on('console', (m) => { + if (m.type() === 'error') errors.push('console: ' + m.text() + ' @ ' + m.location().url) + }) + // ERR_ABORTED is an expected browser cancellation when deliberately navigating; all other failures are fatal. + page.on('requestfailed', (req) => { + if (req.failure()?.errorText !== 'net::ERR_ABORTED') + errors.push('request: ' + req.url() + ' ' + req.failure()?.errorText) + }) + page.on('response', (r) => { + if (r.status() >= 400) errors.push('HTTP ' + r.status() + ' ' + r.url()) + }) + page.on('request', (r) => requests.push(r.url())) + const prefix = locale === 'ko' ? '' : '/en' + try { + for (const route of ['/hire', '/about', '']) { + const response = await page.goto(base + prefix + route, { waitUntil: 'networkidle', timeout: 30000 }) + assert(response?.ok(), 'route unavailable: ' + route) + await page.evaluate(() => document.fonts.ready) + assert((await page.locator('h1').count()) === 1, 'Expected one H1 at ' + route) + assert( + !(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1)), + 'Horizontal overflow at ' + route + ) + if (route === '/hire') { + const contact = page + .getByRole('link', { name: locale === 'ko' ? /이메일로 연락하기/ : /Email me/ }) + .first() + assert(await contact.isVisible(), 'Above-fold contact CTA missing') + const rect = await contact.boundingBox() + assert(rect && rect.y < (width === 390 ? 844 : 1000), 'Contact CTA not visible in initial viewport') + assert(await page.locator('a[href="#resume"]').isVisible(), 'Résumé shortcut missing') + assert((await page.locator('#resume').count()) === 1, 'Résumé anchor missing') + const approach = page.locator('#delivery-method') + assert(await approach.isVisible(), 'Evidence-led delivery method missing') + assert( + (await approach.locator('a[href*="/projects/"]').count()) === 3, + 'Delivery examples must link three real case studies' + ) + const primary = page.locator('a[href="/resume/Sihyeong-Lee-Resume.pdf"]') + assert(await primary.isVisible(), 'Primary résumé CTA missing') + const colors = await primary.evaluate((el) => ({ + bg: getComputedStyle(el).backgroundColor, + fg: getComputedStyle(el).color, + })) + assert(colors.bg !== 'rgba(0, 0, 0, 0)', 'Résumé CTA background is transparent') + const a = luminance(colors.bg), + b = luminance(colors.fg) + const contrast = (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05) + assert(contrast >= 4.5, 'Résumé CTA contrast ' + contrast.toFixed(2)) + assert(await page.locator('#engineering-evidence').isVisible(), 'Evidence section missing') + assert((await page.locator('a[href*="/blob/"]').count()) >= 2, 'Pinned evidence links missing') + await page.screenshot({ path: path.join(output, label + '-hire.png'), fullPage: true }) + results.push({ label, route, contrast: Number(contrast.toFixed(2)), screenshot: label + '-hire.png' }) + } + if (route === '/about') + assert( + (await page.locator('body').innerText()).includes( + locale === 'ko' ? 'AI / 에이전트 시스템' : 'AI / Agent Systems' + ), + 'AI skill category missing' + ) + if (!route) { + const proof = page.locator('#verified-work') + assert(await proof.isVisible(), 'Source-pinned proof section missing') + assert((await proof.locator('article').count()) === 2, 'Expected both pinned evidence records') + assert( + (await proof.locator('a[href*="/blob/"]').count()) === 2, + 'Evidence links must resolve to pinned Git commits' + ) + const link = page.locator('a[href="' + prefix + '/projects/studios"]').first() + assert((await link.count()) === 1, 'Private studios card has no case-study link') + await link.click() + await page.waitForURL('**' + prefix + '/projects/studios') + assert( + (await page.locator('h1').innerText()).toLowerCase().includes('studios'), + 'Case study navigation failed' + ) + } + } + assert( + !requests.some((url) => /_vercel\/(?:insights|speed-insights)/.test(url)), + 'Local preview requested Vercel-only endpoints' + ) + assert(errors.length === 0, errors.join(' | ')) + console.log('PASS', label, 'home/hire/about/case-study; console/network clean') + } catch (e) { + failures.push(label + ': ' + (e instanceof Error ? e.message : String(e))) + await page.screenshot({ path: path.join(output, label + '-failure.png'), fullPage: true }).catch(() => {}) + } finally { + await ctx.close() + } + } + const ctx = await browser.newContext() + const page = await ctx.newPage() + for (const prefix of ['', '/en']) + for (const route of ['/projects/creator-learning-os', '/blog/agent-engineering-ownership']) { + const r = await page.goto(base + prefix + route, { waitUntil: 'domcontentloaded' }) + assert(r?.ok() && (await page.locator('h1').count()) === 1, 'New content failed: ' + route) + results.push({ route: prefix + route, status: r?.status() }) + } + for (const prefix of ['', '/en']) { + const incidentCtx = await browser.newContext() + const incidentPage = await incidentCtx.newPage() + const route = prefix + '/projects/devdeck' + const response = await incidentPage.goto(base + route, { waitUntil: 'networkidle', timeout: 30000 }) + assert(response?.ok(), 'DevDeck failure case route missing: ' + route) + const caseStudy = incidentPage.locator('#failure-investigation') + assert(await caseStudy.isVisible(), 'Visible failure investigation missing: ' + route) + assert((await caseStudy.locator('img').count()) === 2, 'Expected before/after source screenshot crops') + assert( + (await caseStudy.locator('a[href*="github.com/writingdeveloper/devdeck/"]').count()) === 6, + 'Need six immutable source and CI links' + ) + const body = await caseStudy.innerText() + assert(body.includes('20/20') && body.includes('8/8'), 'Case study test counts missing') + assert( + body.includes(prefix ? 'What this does not prove' : '검증하지 않은 범위'), + 'Unverified scope disclaimer missing: ' + route + ' heading=' + (await caseStudy.locator('h2').innerText()) + ) + for (const img of await caseStudy.locator('img').all()) { + await img.scrollIntoViewIfNeeded() + const loaded = await img.evaluate((el) => el instanceof HTMLImageElement && el.complete && el.naturalWidth > 0) + assert(loaded, 'QA fixture screenshot failed to load') + } + results.push({ route, status: response?.status(), imagesLoaded: 2, pinnedLinks: 6, incident: true }) + console.log('PASS INCIDENT', route, 'before/after images, 6 source links, scope') + await incidentCtx.close() + } + for (const prefix of ['', '/en']) { + const freshContext = await browser.newContext() + const freshPage = await freshContext.newPage() + const npcRoute = prefix + '/projects/brain-vat-siege' + const npcResponse = await freshPage.goto(base + npcRoute, { waitUntil: 'networkidle', timeout: 30000 }) + assert(npcResponse?.ok(), 'BRAIN//VAT project route unavailable') + const npcEvidence = freshPage.locator('#verified-demo') + assert(await npcEvidence.isVisible(), 'Verified NPC demo missing') + const liveNpc = npcEvidence.locator('a[href="https://brain-vat-siege.vercel.app/npc"]') + assert((await liveNpc.count()) === 1, 'NPC live demonstration link missing') + const npcImage = npcEvidence.locator('img') + await npcImage.scrollIntoViewIfNeeded() + assert( + await npcImage.evaluate((node) => node instanceof HTMLImageElement && node.complete && node.naturalWidth > 0), + 'Real NPC production screenshot did not load' + ) + const npcText = await npcEvidence.innerText() + assert(npcText.includes('200/200'), 'NPC test-count evidence missing') + assert(npcText.includes(prefix ? 'no trained policy' : '학습된 정책'), 'Missing NPC limitation at ' + npcRoute) + results.push({ route: npcRoute, verifiedDemo: true, screenshotLoaded: true }) + console.log('PASS VERIFIED DEMO', npcRoute) + + const creatorRoute = prefix + '/projects/creator-learning-os' + const creatorResponse = await freshPage.goto(base + creatorRoute, { waitUntil: 'networkidle' }) + assert(creatorResponse?.ok(), 'Creator OS pilot product detail unavailable') + assert( + (await freshPage.locator('a[href="https://creator.writingdeveloper.blog/demo"]').count()) >= 1, + 'Creator OS no-login public demo missing' + ) + const creatorDescription = await freshPage.locator('header').last().innerText() + assert( + creatorDescription.includes(prefix ? 'Testing-only' : '승인된 테스트 사용자'), + 'Creator OS pilot access restrictions missing' + ) + results.push({ route: creatorRoute, pilotStatus: 'testing-only', demoLinked: true }) + console.log('PASS CREATOR PILOT', creatorRoute) + await freshContext.close() + } + for (const file of resumes.files) { + const r = await ctx.request.get(base + '/resume/' + file.file) + assert(r.ok() && r.headers()['content-type']?.includes('application/pdf'), 'PDF response invalid') + assert(sha(await r.body()) === file.sha256, 'Served résumé bytes mismatch: ' + file.file) + results.push({ file: file.file, hashVerified: true }) + console.log('PASS PDF', file.file) + } + await ctx.close() +} catch (e) { + failures.push(e instanceof Error ? e.message : String(e)) +} finally { + await browser.close() + const report = { + checkedAt: new Date().toISOString(), + base, + sourceFingerprint, + status: failures.length ? 'FAIL' : 'PASS', + browser: 'Chromium headless', + humanVisualAcceptance: false, + productionDeploymentVerified: false, + results, + failures, + } + fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify(report, null, 2) + '\n') + console.log(report.status, 'portfolio QA; report:', path.join(output, 'report.json')) + if (failures.length) { + console.error(failures.join('\n')) + process.exitCode = 1 + } +} diff --git a/src/app/[locale]/LedgerHome.tsx b/src/app/[locale]/LedgerHome.tsx index 0b26135..1b8a9b0 100644 --- a/src/app/[locale]/LedgerHome.tsx +++ b/src/app/[locale]/LedgerHome.tsx @@ -3,6 +3,7 @@ import Image from 'next/image' import { useTranslations } from 'next-intl' import { Link } from '@/i18n/navigation' import { PRIMARY_NAV_ITEMS } from '@/components/layout/navigation' +import { EvidenceSnapshotStrip } from '@/components/projects/EvidenceSnapshotStrip' import type { Project, Tombstone } from '@/types/content' import type { PostMeta } from '@/lib/mdx' import { predecessorOf } from '@/lib/lineage' @@ -22,7 +23,7 @@ const CARD = 'var(--l-card)' const ELEV = 'var(--l-elev)' const BORDER = 'var(--l-border)' -const STACK = ['TypeScript', 'Next.js', 'React', 'Three.js', 'NestJS', 'Electron', 'Python'] +const STACK = ['MCP', 'Agent Evals', 'Claude Code', 'Codex', 'TypeScript', 'Python', 'Next.js', 'PostgreSQL'] // YAML frontmatter dates (`publishedAt: 2026-06-10`) parse to a Date at runtime // even though PostMeta types it as string — normalize to a YYYY-MM-DD string so @@ -153,6 +154,13 @@ function FeaturedCard({ project, locale }: { project: Project; locale: string }) )}
+ + {locale === 'ko' ? '사례와 검증 보기' : 'Case study & evidence'} → + {project.website && ( ).map((key) => { @@ -206,7 +215,7 @@ export function LedgerHome({ const stats = [ { n: String(portfolioStats.builds).padStart(2, '0'), label: 'BUILDS', hot: true }, - { n: String(portfolioStats.public).padStart(2, '0'), label: 'LIVE', hot: false }, + { n: String(portfolioStats.public).padStart(2, '0'), label: 'PUBLIC LINKS', hot: false }, { n: String(portfolioStats.playStore).padStart(2, '0'), label: 'ON PLAY', hot: false }, { n: String(portfolioStats.featured).padStart(2, '0'), label: 'FEATURED', hot: false }, ] @@ -248,7 +257,7 @@ export function LedgerHome({ @@ -262,7 +271,7 @@ export function LedgerHome({
{/* Hero */} -
+
- AI-NATIVE PRODUCT ENGINEER + AI-NATIVE PRODUCT · AGENT SYSTEMS ENGINEER LEDGER NO. 001 @@ -293,10 +302,31 @@ export function LedgerHome({

- {locale === 'ko' - ? `AI 네이티브 제품과 개발 인프라를 설계·배포·운영합니다. 에이전트 도구, RAG 평가, 로컬 GPU 생성 파이프라인, 프로덕션 풀스택 시스템까지 직접 책임집니다.` - : `I design, ship, and operate AI-native products and developer infrastructure — from agent tooling and RAG evaluation to local GPU generation pipelines and production full-stack systems.`} + {tHome('hero.description')}

+
+ + {locale === 'ko' ? '경력 · 이력서 보기' : 'Experience & résumé'} → + + + {locale === 'ko' ? 'AI 파이프라인 사례' : 'Inside the AI pipeline'} → + + + {locale === 'ko' ? '검증된 작업 사례 보기' : 'Explore selected work'} → + +
{/* Stats ribbon */} @@ -316,10 +346,19 @@ export function LedgerHome({ ))}
+

+ {locale === 'ko' + ? '전체 제작 수에는 개발 중인 시제품도 포함됩니다. 공개 링크 수는 상용 출시·실사용자·매출을 의미하지 않습니다.' + : 'Builds include unfinished prototypes. Public destinations do not establish a commercial launch, active users, or revenue.'} +

+ + {/* Evidence is pinned to reviewed commits; project counts also include prototypes. */} + + {/* Featured */}
- +
{groups.map(({ key, items }) => (
@@ -355,7 +394,9 @@ export function LedgerHome({
- {/* Counts come from the ledger, never from prose. */} +
+

+ {t('approach.heading')} +

+

{t('approach.intro')}

+
+ {DELIVERY_STAGES.map((stage, index) => ( +
+ 0{index + 1} +

+ {t(`approach.${stage.key}.title`)} +

+

+ {t(`approach.${stage.key}.body`)} +

+ + {t(`approach.${stage.key}.link`)} → + +
+ ))} +
+
+ + + + {/* Counts are a secondary inventory, not proof of quality. */}

{t('evidence.heading')}

diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index e83d94b..9d036fe 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -14,6 +14,7 @@ import { SpeedInsights } from '@vercel/speed-insights/next' import { GoogleAdSense } from '@/components/analytics/GoogleAdSense' import { GoogleAnalytics } from '@/components/analytics/GoogleAnalytics' import { OutboundTracker } from '@/components/analytics/OutboundTracker' +import { telemetryPolicy } from '@/lib/telemetry-policy' import '../globals.css' // Noto Sans KR is gone. It was added (FOLIO-22) as *the* Korean face, before @@ -112,6 +113,10 @@ export default async function LocaleLayout({ const messages = await getMessages() const headersList = await headers() const nonce = headersList.get('x-nonce') ?? undefined + const telemetry = telemetryPolicy( + { VERCEL_ENV: process.env.VERCEL_ENV, PORTFOLIO_ENABLE_TELEMETRY: process.env.PORTFOLIO_ENABLE_TELEMETRY }, + headersList.get('host') + ) return ( @@ -136,13 +141,13 @@ export default async function LocaleLayout({ {/* Third-party analytics/ads load after hydration (next/script afterInteractive) so they stay off the critical render path. */} - + {telemetry.thirdParty && } {/* One delegated listener turns departures (demo, code, store, mail) into named GA4 events, without making any card a client component. */} - - - - + {telemetry.thirdParty && } + {telemetry.thirdParty && } + {telemetry.vercel && } + {telemetry.vercel && } ) diff --git a/src/app/[locale]/play/sections/SkillsSection.tsx b/src/app/[locale]/play/sections/SkillsSection.tsx index 2de02ae..99d53ce 100644 --- a/src/app/[locale]/play/sections/SkillsSection.tsx +++ b/src/app/[locale]/play/sections/SkillsSection.tsx @@ -2,13 +2,15 @@ import type { Skill } from '@/types/content' import { skillCategories } from '@/types/content' +import { groupSkills } from '@/lib/skill-groups' import { GlowOrb } from '../components/GlowOrb' import { SectionLabel } from '../components/SectionLabel' import { SectionDivider } from '../components/SectionDivider' import { SECTION_SPACING } from '../scene/CameraRig' import { categoryX } from '@/lib/skills-layout' -const CATEGORY_COLORS: Record = { +const CATEGORY_COLORS: Record = { + ai: '#c6f24e', frontend: '#7b6f99', backend: '#8b7355', tools: '#6b6891', @@ -24,12 +26,7 @@ interface SkillsSectionProps { export function SkillsSection({ skills, locale, sectionLabel }: SkillsSectionProps) { const sectionY = -SECTION_SPACING * 2 - const grouped = { - frontend: skills.filter((s) => s.category === 'frontend'), - backend: skills.filter((s) => s.category === 'backend'), - tools: skills.filter((s) => s.category === 'tools'), - infra: skills.filter((s) => s.category === 'infra'), - } + const grouped = groupSkills(skills) const categoryLabel = (cat: string) => skillCategories[cat as keyof typeof skillCategories]?.[locale as 'ko' | 'en'] ?? cat @@ -54,7 +51,7 @@ export function SkillsSection({ skills, locale, sectionLabel }: SkillsSectionPro key={skill.name} position={[x, y, z]} label={skill.name} - color={CATEGORY_COLORS[category] ?? '#7c6cf0'} + color={CATEGORY_COLORS[category as Skill['category']]} /> ) })} diff --git a/src/app/[locale]/projects/[slug]/page.tsx b/src/app/[locale]/projects/[slug]/page.tsx index d41f42e..438c6fb 100644 --- a/src/app/[locale]/projects/[slug]/page.tsx +++ b/src/app/[locale]/projects/[slug]/page.tsx @@ -12,6 +12,8 @@ import { predecessorOf } from '@/lib/lineage' import { getAllPosts } from '@/lib/mdx' import { buildProjectPostMap } from '@/lib/post-project-links' import { PageTransition } from '@/components/ui/PageTransition' +import { FailureInvestigation } from '@/components/projects/FailureInvestigation' +import { VerifiedProjectDemo } from '@/components/projects/VerifiedProjectDemo' import { generateBreadcrumbJsonLd, generateProjectJsonLd, safeJsonLd, toAbsoluteUrl } from '@/lib/seo' const PROJECTS = projectsData.projects as Project[] @@ -223,6 +225,10 @@ export default async function ProjectDetailPage({ params }: { params: Promise<{
)} + + + +
{/* Sits first: for a private project with no website this is the only place on the page that goes anywhere at all. */} diff --git a/src/app/[locale]/projects/page.tsx b/src/app/[locale]/projects/page.tsx index 2200bbf..4c29931 100644 --- a/src/app/[locale]/projects/page.tsx +++ b/src/app/[locale]/projects/page.tsx @@ -7,7 +7,7 @@ import { SITE_URL } from '@/lib/constants' import { ProjectCard } from '@/components/projects/ProjectCard' import { PageTransition } from '@/components/ui/PageTransition' import { generateBreadcrumbJsonLd, generateProjectListJsonLd, safeJsonLd, toAbsoluteUrl } from '@/lib/seo' -import { APP_CATEGORY, partitionProjects } from '@/lib/projects' +import { APP_CATEGORY, partitionProjects, projectExcerpt } from '@/lib/projects' import { getAllPosts } from '@/lib/mdx' import { buildProjectPostMap, type LinkedPost } from '@/lib/post-project-links' import { useLocale } from 'next-intl' @@ -70,7 +70,7 @@ function ProjectsContent({ projectPosts }: { projectPosts: Map ({ name: project.name, - description: locale === 'ko' ? project.descriptionKo : project.descriptionEn, + description: projectExcerpt(locale === 'ko' ? project.descriptionKo : project.descriptionEn), url: project.website ?? (!project.private && project.github ? project.github : undefined), techStack: project.techStack, ...(project.screenshot ? { image: toAbsoluteUrl(project.screenshot) } : {}), diff --git a/src/components/projects/EvidencePanel.tsx b/src/components/projects/EvidencePanel.tsx new file mode 100644 index 0000000..d5f84b2 --- /dev/null +++ b/src/components/projects/EvidencePanel.tsx @@ -0,0 +1,41 @@ +import { useLocale } from 'next-intl' +import manifest from '../../../content/evidence.json' +import { evidenceUrl } from '@/lib/evidence-policy' + +export function EvidencePanel() { + const ko = useLocale() === 'ko' + return ( +
+

+ {ko ? '검증 범위가 있는 엔지니어링 증거' : 'Engineering evidence, with scope'} +

+

+ {ko + ? '숫자만 제시하지 않습니다. 기록 시점, 소스 버전, 검증하지 않은 범위를 함께 남깁니다.' + : 'Each figure is tied to a dated source version and states what was not validated.'} +

+
+ {manifest.records.map((record) => ( + + ))} +
+
+ ) +} diff --git a/src/components/projects/EvidenceSnapshotStrip.tsx b/src/components/projects/EvidenceSnapshotStrip.tsx new file mode 100644 index 0000000..229c964 --- /dev/null +++ b/src/components/projects/EvidenceSnapshotStrip.tsx @@ -0,0 +1,96 @@ +import { Link } from '@/i18n/navigation' +import evidence from '../../../content/evidence.json' +import { evidenceUrl } from '@/lib/evidence-policy' + +/** + * Public claims are read from the exact reviewed manifest used by /hire. + * Never copy a passing-test figure into free-form copy or imply a live release. + */ +export function EvidenceSnapshotStrip({ locale }: { locale: string }) { + const ko = locale === 'ko' + + return ( +
+
+
+ + {ko ? '§01 · 근거가 남는 작업' : '§01 · PROOF OVER PROMPTS'} + +

+ {ko ? 'AI 사용보다 검증 가능한 결과' : 'What survives verification'} +

+

+ {ko + ? '생성한 코드의 양이 아니라, 고정된 소스·측정 조건·검증하지 못한 범위를 함께 공개합니다.' + : 'Not how much code an AI helped produce. Each record has a source commit, dated measurement, and explicit limits.'} +

+
+ + {ko ? '근거의 범위 확인' : 'Review the evidence scope'} → + +
+
+ {evidence.records.map((record) => ( +
+
+ + {record.project} + + +
+

+ {ko ? record.titleKo : record.titleEn} +

+

+ {record.value} +

+

+ {ko ? record.scopeKo : record.scopeEn} +

+
+ + {ko ? '문제·결정·결과' : 'Problem, decision, result'} → + + + {ko ? '고정된 원본' : 'Pinned source'} ↗ + +
+
+ ))} +
+
+ ) +} diff --git a/src/components/projects/FailureInvestigation.tsx b/src/components/projects/FailureInvestigation.tsx new file mode 100644 index 0000000..fbd2c22 --- /dev/null +++ b/src/components/projects/FailureInvestigation.tsx @@ -0,0 +1,195 @@ +import Image from 'next/image' +import reports from '../../../content/failure-cases.json' + +/** Reviewed incident evidence, rendered only on the matching project. */ +export function FailureInvestigation({ projectSlug, locale }: { projectSlug: string; locale: string }) { + const item = reports.incidents.find((entry) => entry.project === projectSlug) + if (!item) return null + const ko = locale === 'ko' + + return ( +
+
+

+ {ko ? '실패 → 원인 → 수정 → 검증' : 'Engineering incident / failure to verification'} +

+

+ {ko ? item.titleKo : item.titleEn} +

+

{ko ? item.introKo : item.introEn}

+
+ + {ko ? item.statusKo : item.statusEn} + + + {ko ? '최초 실패 기록' : 'Baseline recorded'}: + +
+
+ +
+ {[item.before, item.after].map((frame, index) => ( +
+
+ + {index === 0 ? (ko ? '실패' : 'Before') : ko ? '수정 후' : 'After'} + +

{ko ? frame.titleKo : frame.titleEn}

+
+ {ko +
+ {ko ? frame.noteKo : frame.noteEn} +
+
+ ))} +
+ +
+
+

+ {ko ? item.reproduction.titleKo : item.reproduction.titleEn} +

+
    + {(ko ? item.reproduction.stepsKo : item.reproduction.stepsEn).map((step, i) => ( +
  1. + + {String(i + 1).padStart(2, '0')} + + {step} +
  2. + ))} +
+
+
+

{ko ? item.rootCause.titleKo : item.rootCause.titleEn}

+

+ {ko ? item.rootCause.textKo : item.rootCause.textEn} +

+

+ {ko + ? '전송 성공은 실제 화면과 영속 상태가 올바르다는 증거가 아닙니다.' + : 'A successful transport response is not proof that the UI or persistent record is correct.'} +

+
+
+ +
+

+ {ko ? '수정: 상태 소유권·쓰기 순서·초안 보호' : 'Fix: ownership, write ordering, and draft protection'} +

+
+ {item.changes.map((change, i) => ( +
+ {String(i + 1).padStart(2, '0')} +

{ko ? change.titleKo : change.titleEn}

+

+ {ko ? change.bodyKo : change.bodyEn} +

+
+ ))} +
+
+ +
+

+ {ko ? item.verification.titleKo : item.verification.titleEn} +

+
+
+

+ {item.verification.focusedTests}/{item.verification.focusedTests} +

+

+ {ko ? '관련 단위 테스트 통과' : 'Focused unit tests passed'} +

+
+
+

+ {item.verification.electronJourneys}/{item.verification.electronJourneys} +

+

+ {ko ? 'Electron 3개 앱 검증 통과' : 'Three-app Electron journeys passed'} +

+
+
+

+ {ko ? '재실행' : 'Rerun'}: .{' '} + {ko ? item.verification.environmentKo : item.verification.environmentEn} +

+
    + {item.verification.journeys.map((journey) => ( +
  • + + {ko ? journey.ko : journey.en} +
  • + ))} +
+

{ko ? '재검증 명령' : 'Reproduce the checks'}

+
+ {item.verification.commands.map((command) => ( + + {command} + + ))} +
+
+ + + + +
+ ) +} diff --git a/src/components/projects/ProjectCard.tsx b/src/components/projects/ProjectCard.tsx index 0bedd0e..186f641 100644 --- a/src/components/projects/ProjectCard.tsx +++ b/src/components/projects/ProjectCard.tsx @@ -3,7 +3,7 @@ import { useTranslations, useLocale } from 'next-intl' import Image from 'next/image' import type { Project } from '@/types/content' import { predecessorOf } from '@/lib/lineage' -import { projectHref } from '@/lib/projects' +import { projectHref, projectExcerpt } from '@/lib/projects' interface ProjectCardProps { project: Project @@ -78,7 +78,7 @@ export function ProjectCard({ project, priority = false, relatedPost }: ProjectC
-
+
{/* The name is the route into the project's own page — every card needs one inbound link or those pages are orphans. */}

@@ -100,8 +100,10 @@ export function ProjectCard({ project, priority = false, relatedPost }: ProjectC )}

{project.descriptionKo && ( -

- {locale === 'en' ? project.descriptionEn || project.descriptionKo : project.descriptionKo} +

+ {projectExcerpt( + locale === 'en' ? project.descriptionEn || project.descriptionKo : project.descriptionKo + )}

)}
@@ -109,7 +111,7 @@ export function ProjectCard({ project, priority = false, relatedPost }: ProjectC {project.techStack?.length > 0 && (
- {project.techStack.map((tech) => ( + {project.techStack.slice(0, 6).map((tech) => ( ))} + {project.techStack.length > 6 && ( + + +{project.techStack.length - 6} + + )}
)} diff --git a/src/components/projects/VerifiedProjectDemo.tsx b/src/components/projects/VerifiedProjectDemo.tsx new file mode 100644 index 0000000..822444a --- /dev/null +++ b/src/components/projects/VerifiedProjectDemo.tsx @@ -0,0 +1,68 @@ +import Image from 'next/image' +import demos from '../../../content/verified-demos.json' + +/** A dated, externally usable demonstration, with its measurement limits attached. */ +export function VerifiedProjectDemo({ projectSlug, locale }: { projectSlug: string; locale: string }) { + const item = demos.demos.find((demo) => demo.project === projectSlug) + if (!item) return null + const ko = locale === 'ko' + return ( +
+
+
+ + {ko ? '실제 운영 검증 데모' : 'Verified public demonstration'} + + + + +
+

+ {ko ? item.titleKo : item.titleEn} +

+

+ {ko ? item.descriptionKo : item.descriptionEn} +

+ + {ko ? '공개 NPC 데모 직접 실행' : 'Try the live NPC demonstration'} ↗ + +
+
+ {ko +
+ {ko ? item.captureNoteKo : item.captureNoteEn} +
+
+
+
+

{ko ? '검증된 범위' : 'What passed'}

+

+ {ko ? item.testLineKo : item.testLineEn} +

+
+
+

{ko ? '아직 증명하지 않은 범위' : 'What was not validated'}

+

+ {ko ? item.caveatKo : item.caveatEn} +

+
+
+
+ ) +} diff --git a/src/components/ui/SkillGrid.tsx b/src/components/ui/SkillGrid.tsx index 424dfba..25f95b8 100644 --- a/src/components/ui/SkillGrid.tsx +++ b/src/components/ui/SkillGrid.tsx @@ -1,6 +1,7 @@ import { useLocale } from 'next-intl' import type { Skill } from '@/types/content' import { skillCategories } from '@/types/content' +import { groupSkills } from '@/lib/skill-groups' /** * The stack, grouped by category. @@ -12,12 +13,7 @@ import { skillCategories } from '@/types/content' */ export function SkillGrid({ skills }: { skills: Skill[] }) { const locale = useLocale() - const grouped = { - frontend: skills.filter((s) => s.category === 'frontend'), - backend: skills.filter((s) => s.category === 'backend'), - tools: skills.filter((s) => s.category === 'tools'), - infra: skills.filter((s) => s.category === 'infra'), - } + const grouped = groupSkills(skills) return (
diff --git a/src/lib/__tests__/ai-differentiation-2026.test.ts b/src/lib/__tests__/ai-differentiation-2026.test.ts new file mode 100644 index 0000000..c680302 --- /dev/null +++ b/src/lib/__tests__/ai-differentiation-2026.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import en from '../../../messages/en.json' +import ko from '../../../messages/ko.json' +import projectsData from '../../../content/projects.json' +import evidence from '../../../content/evidence.json' + +const source = (filename: string) => readFileSync(join(process.cwd(), filename), 'utf8') + +describe('2026 portfolio: distinguish judgment and evidence from AI tool adoption', () => { + it('explains acceptance and failure ownership in both language variants', () => { + expect(en.home.hero.description).toContain('acceptance criteria') + expect(en.home.hero.description).toContain('safe releases') + expect(ko.home.hero.description).toContain('완료 기준') + expect(ko.home.hero.description).toContain('안전한 출시') + expect(source('src/app/[locale]/LedgerHome.tsx')).toContain("tHome('hero.description')") + }) + + it('renders the same source-pinned evidence records used by Hire', () => { + const home = source('src/app/[locale]/LedgerHome.tsx') + const strip = source('src/components/projects/EvidenceSnapshotStrip.tsx') + expect(home).toContain('') + expect(strip).toContain('evidence.records.map') + expect(strip).toContain('record.snapshotAt') + expect(strip).toContain('record.scopeKo') + expect(strip).toContain('record.scopeEn') + expect(strip).toContain('evidenceUrl(record)') + for (const record of evidence.records) { + expect(record.source.commit).toMatch(/^[0-9a-f]{40}$/) + expect(record.scopeEn.length).toBeGreaterThan(35) + expect(record.scopeKo.length).toBeGreaterThan(20) + } + }) + + it('never claims every ledger project is a released public demo', () => { + expect(projectsData.projects.some((p) => p.private && !p.website)).toBe(true) + expect(en.projects.metaDescription).not.toContain('each with a live demo or store link') + expect(en.projects.fullLedgerDescription).toContain('not a release') + expect(ko.projects.fullLedgerDescription).toContain('출시 수') + expect(en.hire.evidence.shipped).toContain('not necessarily a commercial launch') + }) + + it('makes the AI-assisted delivery approach concrete in both locales', () => { + const hire = source('src/app/[locale]/hire/page.tsx') + expect(hire).toContain('id="delivery-method"') + expect(hire).toContain('DELIVERY_STAGES.map') + expect(en.hire.approach.heading).toContain('AI-generated') + expect(ko.hire.approach.heading).toContain('검증 가능한') + for (const key of ['decision', 'verification', 'release'] as const) { + expect(en.hire.approach[key].body.length).toBeGreaterThan(55) + expect(ko.hire.approach[key].body.length).toBeGreaterThan(25) + } + }) + + it('keeps proof links to three actual case-study pages', () => { + const hire = source('src/app/[locale]/hire/page.tsx') + for (const slug of ['devdeck', 'citefirst', 'creator-os']) { + expect(projectsData.projects.find((p) => p.slug === slug)?.caseStudy).toBeTruthy() + expect(hire).toContain(`/projects/${slug}`) + } + }) +}) diff --git a/src/lib/__tests__/devdeck-failure-case.test.ts b/src/lib/__tests__/devdeck-failure-case.test.ts new file mode 100644 index 0000000..932ddfe --- /dev/null +++ b/src/lib/__tests__/devdeck-failure-case.test.ts @@ -0,0 +1,75 @@ +import { statSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import cases from '../../../content/failure-cases.json' +import projects from '../../../content/projects.json' + +const incident = cases.incidents[0] +const baseline = 'b7817547dd2100751ce46229a83810319affe82b' +const candidate = 'fa8864833b98e7d81f140e598b1f56d4d2b60c39' + +describe('DevDeck source-backed failure reconstruction', () => { + it('describes exactly one case anchored to a real, existing project', () => { + expect(cases.schemaVersion).toBe(1) + expect(cases.incidents).toHaveLength(1) + expect(incident.project).toBe('devdeck') + expect(projects.projects.some((project) => project.slug === incident.project)).toBe(true) + expect(incident.baselineCommit).toBe(baseline) + expect(incident.candidateCommit).toBe(candidate) + }) + + it('distinguishes the historical failure from the Oct 12 rerun and public release', () => { + expect(incident.recordedAt).toBe('2026-09-08') + expect(incident.verifiedAt).toBe('2026-10-12') + expect(incident.statusEn).toContain('not a public release') + expect(incident.statusKo).toContain('정식 출시 아님') + expect(incident.verification.limitationsEn).toContain('Not two physical machines') + expect(incident.verification.limitationsKo).toContain('물리적으로 분리된') + }) + + it('has the reproduced user-visible failure and a specific root cause in both locales', () => { + for (const [steps, cause, before, after] of [ + [incident.reproduction.stepsEn, incident.rootCause.textEn, incident.before.noteEn, incident.after.noteEn], + [incident.reproduction.stepsKo, incident.rootCause.textKo, incident.before.noteKo, incident.after.noteKo], + ] as const) { + expect(steps).toHaveLength(4) + expect(cause).toContain('LOCAL_MACHINE_ID') + expect(before.length).toBeGreaterThan(25) + expect(after.length).toBeGreaterThan(25) + } + expect(incident.changes).toHaveLength(3) + expect(incident.verification.commands).toEqual([ + 'npm test -- src/main/sessionLabels.test.ts src/shared/sessionConsistency.test.ts', + 'npm run qa:session-sync', + ]) + }) + + it('shows the actually executed 20 focused tests and eight independent Electron journeys', () => { + expect(incident.verification.focusedTests).toBe(20) + expect(incident.verification.electronJourneys).toBe(8) + expect(incident.verification.journeys).toHaveLength(8) + expect(new Set(incident.verification.journeys.map((journey) => journey.en)).size).toBe(8) + for (const journey of incident.verification.journeys) expect(journey.ko.length).toBeGreaterThan(10) + }) + + it('only publishes small, locally redacted fixture crops; never private paths', () => { + for (const photo of [incident.before, incident.after]) { + expect(photo.image).toMatch(/^\/images\/projects\/devdeck-sync-qa-(before|after)\.webp$/) + const size = statSync(join(process.cwd(), 'public', photo.image.slice(1))).size + expect(size).toBeGreaterThan(2000) + expect(size).toBeLessThan(90000) + expect(photo.noteEn.toLowerCase()).toContain('crop') + } + }) + + it('links only to immutable GitHub sources and a same-commit successful CI run', () => { + const kinds = incident.sources.map((source) => source.kind) + expect(kinds).toEqual(['baseline', 'fix', 'review', 'unit', 'harness', 'ci']) + for (const source of incident.sources) { + expect(source.href).toMatch(/^https:\/\/github\.com\/writingdeveloper\/devdeck\//) + if (source.kind === 'baseline') expect(source.href).toContain(baseline) + else if (source.kind === 'ci') expect(source.href).toContain('34319241204') + else expect(source.href).toContain(candidate) + } + }) +}) diff --git a/src/lib/__tests__/employment-positioning.test.ts b/src/lib/__tests__/employment-positioning.test.ts index 70a9dd1..3d5951a 100644 --- a/src/lib/__tests__/employment-positioning.test.ts +++ b/src/lib/__tests__/employment-positioning.test.ts @@ -8,17 +8,17 @@ import ko from '../../../messages/ko.json' describe('employment positioning', () => { it('leads the home page with the AI-native product-engineer positioning', () => { const home = readFileSync(join(process.cwd(), 'src/app/[locale]/LedgerHome.tsx'), 'utf8') - expect(home).toContain('AI-NATIVE PRODUCT ENGINEER') + expect(home).toContain('AI-NATIVE PRODUCT · AGENT SYSTEMS ENGINEER') expect(home).not.toContain('SOLO FULL-STACK BUILDER') - expect(home).toContain('AI-native products and developer infrastructure') + expect(home).toContain("tHome('hero.description')") expect(home).toContain('SELECTED_WORK.map') }) it('keeps the AI-native identity while targeting revenue-facing engineering work in English', () => { - expect(en.home.metaDescription).toContain('AI-native product engineer') - expect(en.about.metaTitle).toContain('AI-Native Product Engineer') + expect(en.home.metaDescription).toContain('AI-native product and agent-systems engineer') + expect(en.about.metaTitle).toContain('AI-Native Product & Agent Systems Engineer') expect(en.hire.metaTitle).toContain('Browser Automation') - expect(en.hire.badgeRemote).toBe('Los Angeles · Fully remote · US permanent resident') + expect(en.hire.badgeRemote).toBe('Los Angeles · Remote-first · LA hybrid/on-site · US permanent resident') expect(en.hire.fulltime.roles).toBe( 'Senior Software Engineer · AI Integration / Applied AI Engineer · Senior Full-Stack / Automation Engineer' ) @@ -50,10 +50,10 @@ describe('employment positioning', () => { }) it('keeps the Korean positioning aligned with the English profile', () => { - expect(ko.home.metaDescription).toContain('AI 네이티브 제품 엔지니어') - expect(ko.about.metaTitle).toContain('AI 네이티브 제품 엔지니어') + expect(ko.home.metaDescription).toContain('AI 네이티브 제품·에이전트 시스템 엔지니어') + expect(ko.about.metaTitle).toContain('AI 네이티브 제품 · 에이전트 시스템 엔지니어') expect(ko.hire.metaTitle).toContain('브라우저 자동화') - expect(ko.hire.badgeRemote).toBe('로스앤젤레스 · 전면 원격 · 미국 영주권자') + expect(ko.hire.badgeRemote).toBe('로스앤젤레스 · 원격 우선 · LA 하이브리드/출근 가능 · 미국 영주권자') expect(ko.hire.fulltime.roles).toBe( '시니어 소프트웨어 엔지니어 · AI 통합 / Applied AI 엔지니어 · 시니어 풀스택 / 자동화 엔지니어' ) diff --git a/src/lib/__tests__/evidence-policy.test.ts b/src/lib/__tests__/evidence-policy.test.ts new file mode 100644 index 0000000..7289306 --- /dev/null +++ b/src/lib/__tests__/evidence-policy.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' +import data from '../../../content/evidence.json' +import { evidenceProblems, evidenceUrl, evidenceValue, extractMeasurements } from '../evidence-policy' +const now = new Date('2026-09-22T23:59:00Z') +describe('evidence policy', () => { + it('accepts the curated, dated snapshots', () => expect(evidenceProblems(data, now)).toEqual([])) + it('rejects mutable branch refs', () => { + const m = structuredClone(data) + m.records[0].source.commit = 'main' + expect(evidenceProblems(m, now)).toContain('Source must use a full commit SHA') + }) + it('rejects arbitrary paths and private sources', () => { + const m = structuredClone(data) + m.records[0].source.path = '../../.env' + expect(evidenceProblems(m, now).some((x) => x.includes('allowlisted'))).toBe(true) + }) + it('rejects duplicate records', () => { + const m = structuredClone(data) + m.records.push(m.records[0]) + expect(evidenceProblems(m, now).some((x) => x.includes('Duplicate'))).toBe(true) + }) + it('rejects future dates and unscoped claims', () => { + const m = structuredClone(data) + m.records[0].snapshotAt = '2027-01-01' + m.records[0].scopeKo = '' + expect(evidenceProblems(m, now)).toContain('Invalid/future evidence date') + expect(evidenceProblems(m, now)).toContain('Missing localized evidence/scope') + }) + it('does not permit changing the displayed count independently', () => { + const m = structuredClone(data) + m.records[0].value = '999,999 passed' + expect(evidenceProblems(m, now)).toContain('Display value differs from measurements') + }) + it('ties visible numbers to their measurement record', () => { + for (const r of data.records) { + expect(evidenceValue(r)).toBe(r.value) + expect(evidenceUrl(r)).toContain(r.source.commit) + } + }) + it('extracts pass and skip counts from the exact validation row', () => { + expect( + extractMeasurements( + 'devdeck-maintenance', + '| Unit suite including validation, CAS and real TLS conflict | 108 files; 1,289 passed, 1 pre-existing skip |' + ) + ).toEqual({ passed: 1289, skipped: 1 }) + }) + it('retains the evaluation denominator, recall and MRR', () => { + expect( + extractMeasurements( + 'citefirst-retrieval', + '56 questions, in the shipped configuration\n| recall@5 | **0.982** |\n| MRR@10 | **0.785** |' + ) + ).toEqual({ questions: 56, recall5: 0.982, mrr10: 0.785 }) + }) + it('fails on an unknown format rather than manufacturing a metric', () => { + expect(() => extractMeasurements('devdeck-maintenance', 'all tests great')).toThrow() + expect(() => extractMeasurements('citefirst-retrieval', 'no numbers')).toThrow() + expect(() => extractMeasurements('unknown', 'anything')).toThrow() + }) +}) + +describe('measurement boundaries', () => { + it('rejects a missing required field without throwing', () => { + const m = structuredClone(data) + Reflect.deleteProperty(m.records[0].measurements, 'passed') + expect(evidenceProblems(m, now)).toContain('Invalid measurements') + }) + it('rejects an out-of-range fraction', () => { + const m = structuredClone(data) + m.records[1].measurements.recall5 = 2 + expect(evidenceProblems(m, now)).toContain('Invalid measurements') + }) + it('rejects a missing denominator', () => { + const m = structuredClone(data) + m.records[1].measurements.questions = 0 + expect(evidenceProblems(m, now)).toContain('Invalid measurements') + }) + it('reads the committed two-configuration table without confusing the baseline', () => { + expect( + extractMeasurements( + 'citefirst-retrieval', + '46 questions, in the shipped configuration\n| recall@5 | 0.957 | **0.978** |\n| MRR@10 | 0.778 | **0.834** |' + ) + ).toEqual({ questions: 46, recall5: 0.978, mrr10: 0.834 }) + }) +}) diff --git a/src/lib/__tests__/evidence-regressions.test.ts b/src/lib/__tests__/evidence-regressions.test.ts new file mode 100644 index 0000000..11f3793 --- /dev/null +++ b/src/lib/__tests__/evidence-regressions.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { readFileSync } from 'node:fs' +import { groupSkills } from '../skill-groups' +import { telemetryPolicy } from '../telemetry-policy' +import { skillCategories, type Skill } from '@/types/content' +import about from '../../../content/about.json' + +describe('skills and preview regressions', () => { + it('renders every skill exactly once with AI first', () => { + const groups = groupSkills(about.skills as Skill[]) + expect(Object.keys(groups)).toEqual(Object.keys(skillCategories)) + expect(Object.keys(groups)[0]).toBe('ai') + expect(Object.values(groups).flat()).toHaveLength(about.skills.length) + expect(groups.ai.some((s) => s.name === 'Claude Code')).toBe(true) + }) + it.each(['src/components/ui/SkillGrid.tsx', 'src/app/[locale]/play/sections/SkillsSection.tsx'])( + 'uses shared categories in %s', + (file) => { + expect(readFileSync(file, 'utf8')).toContain('groupSkills(skills)') + } + ) + it.each([null, 'localhost:3100', '100.96.248.54:3100', 'branch.vercel.app', 'writingdeveloper.blog.evil.example'])( + 'keeps telemetry off at %s', + (host) => { + expect(telemetryPolicy({ VERCEL_ENV: 'production' }, host)).toEqual({ thirdParty: false, vercel: false }) + } + ) + it('keeps preview deployments silent even on a canonical host', () => { + expect(telemetryPolicy({ VERCEL_ENV: 'preview' }, 'writingdeveloper.blog').vercel).toBe(false) + }) + it('enables production Vercel analytics only on the public host', () => { + expect(telemetryPolicy({ VERCEL_ENV: 'production' }, 'writingdeveloper.blog')).toEqual({ + thirdParty: true, + vercel: true, + }) + }) + it('allows an explicit public self-host opt-in without Vercel scripts', () => { + expect(telemetryPolicy({ PORTFOLIO_ENABLE_TELEMETRY: '1' }, 'writingdeveloper.blog')).toEqual({ + thirdParty: true, + vercel: false, + }) + }) + it('has a defined résumé button token and case-study route for private work', () => { + const hire = readFileSync('src/app/[locale]/hire/page.tsx', 'utf8') + expect(hire).not.toContain('bg-[var(--accent)]') + expect(hire).toContain('bg-[var(--accent-text)]') + expect(readFileSync('src/app/[locale]/LedgerHome.tsx', 'utf8')).toContain('Case study & evidence') + }) +}) diff --git a/src/lib/__tests__/portfolio-freshness-20261013.test.ts b/src/lib/__tests__/portfolio-freshness-20261013.test.ts new file mode 100644 index 0000000..82209fc --- /dev/null +++ b/src/lib/__tests__/portfolio-freshness-20261013.test.ts @@ -0,0 +1,64 @@ +import { statSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import projects from '../../../content/projects.json' +import type { Project } from '@/types/content' +import demos from '../../../content/verified-demos.json' + +const project = (slug: string) => (projects.projects as Project[]).find((value) => value.slug === slug) + +describe('verified portfolio refresh: 2026-10-13', () => { + it('references the actual October 13 public BRAIN//VAT NPC proof, not an LLM claim', () => { + const brain = project('brain-vat-siege') + const item = demos.demos.find((entry) => entry.project === 'brain-vat-siege') + expect(brain?.updatedAt).toBe('2026-10-13') + expect(brain?.status).toBe('active') + expect(brain?.descriptionEn).toContain('deterministic rule-driven') + expect(brain?.caseStudy?.evidenceEn).toContain('200/200') + expect(item).toBeDefined() + expect(item?.sourceCommit).toBe('854213620980fe9c62ea72c12c8d58ec93c9c2e9') + expect(item?.verifiedAt).toBe('2026-10-13') + expect(item?.demoUrl).toBe('https://brain-vat-siege.vercel.app/npc') + expect(item?.descriptionEn).toContain('rule-based') + expect(item?.caveatEn).toContain('no trained policy') + expect(item?.caveatKo).toContain('학습된 정책') + expect(statSync(join(process.cwd(), 'public', item!.image.slice(1))).size).toBeGreaterThan(10000) + }) + + it('describes Creator OS as a testing-only external pilot rather than an unreleased OAuth prototype', () => { + const pilot = project('creator-learning-os') + expect(pilot?.updatedAt).toBe('2026-10-08') + expect(pilot?.private).toBe(true) + expect(pilot?.status).toBe('building') + expect(pilot?.website).toBe('https://creator.writingdeveloper.blog/demo') + expect(pilot?.descriptionEn).toContain('Testing-only') + expect(pilot?.descriptionEn).toContain('not unrestricted signup') + expect(pilot?.descriptionKo).toContain('승인된 테스트 사용자') + expect(pilot?.caseStudy?.evidenceEn).toContain('without a provider model call') + }) + + it('marks Creator OS product and release-hardening entry as two views of one product', () => { + const product = project('creator-learning-os') + const engineering = project('creator-os') + expect(product?.name).toContain('Product & External Pilot') + expect(engineering?.name).toContain('Release Engineering') + expect(engineering?.descriptionEn).toContain('same Creator OS product') + expect(engineering?.caseStudy?.resultKo).toContain('두 번째 제품이 아닌') + }) + + it('does not present the personal Soursea sourcing tool as an offered LLC product', () => { + const tool = project('soursea') + expect(tool?.private).toBe(true) + expect(tool?.website).toBeUndefined() + expect(tool?.name).toContain('Personal Sourcing Workspace') + expect(tool?.descriptionKo).toContain('고객용 서비스 목록과 별개') + }) + + it('leaves unverified updatedAt fields unknown rather than inventing dates', () => { + for (const p of projects.projects as Project[]) { + if (p.updatedAt) expect(p.updatedAt).toMatch(/^20\d{2}-\d{2}-\d{2}$/) + } + expect(project('studios')?.updatedAt).toBeUndefined() + expect(project('kindling')?.updatedAt).toBeUndefined() + }) +}) diff --git a/src/lib/__tests__/projects.test.ts b/src/lib/__tests__/projects.test.ts index 040aafc..a494f92 100644 --- a/src/lib/__tests__/projects.test.ts +++ b/src/lib/__tests__/projects.test.ts @@ -3,6 +3,7 @@ import { getHireStats, getPortfolioStats, getProjectCaseStudySections, + projectExcerpt, hasIndexablePage, HIRE_CASE_STUDIES, HOME_GROUPS, @@ -25,6 +26,18 @@ const p = (name: string, featured: boolean): Project => ({ featured, }) +describe('projectExcerpt', () => { + it('preserves concise text unchanged and trims whitespace', () => { + expect(projectExcerpt(' hello world ')).toBe('hello world') + }) + it('shortens long descriptions without removing the detail page copy', () => { + const text = 'Accessible evidence for a shipped product. '.repeat(20) + const excerpt = projectExcerpt(text, 110) + expect(excerpt.length).toBeLessThanOrEqual(111) + expect(excerpt.endsWith('…')).toBe(true) + }) +}) + describe('sortProjectsFeaturedFirst', () => { it('places featured projects before non-featured', () => { const out = sortProjectsFeaturedFirst([p('A', false), p('B', true), p('C', false), p('D', true)]) @@ -225,16 +238,53 @@ describe('portfolio selection and truthful stats', () => { }) it('keeps selected work to five complementary projects in editorial order', () => { - expect(SELECTED_WORK).toEqual(['devdeck', 'studios', 'rentrights', 'citefirst', 'juicebar']) + expect(SELECTED_WORK).toEqual(['devdeck', 'studios', 'citefirst', 'brain-vat-siege', 'healframe']) expect(new Set(SELECTED_WORK).size).toBe(5) }) }) -describe('JuiceBar ledger refresh', () => { +describe('2026-10 verified source-backed research entries', () => { const ledger = projectsData.projects as Project[] + it('does not conflate unfinished private prototypes with public releases', () => { + for (const slug of ['railforge', 'project-strata', 'music-mv-studio']) { + const project = ledger.find((item) => item.slug === slug) + expect(project).toBeDefined() + expect(project?.status).toBe('building') + expect(project?.private).toBe(true) + expect(project?.website).toBeUndefined() + expect(project?.github).toBeUndefined() + expect(project?.featured).toBe(false) + expect(project?.caseStudy?.resultEn).toBeTruthy() + } + }) +}) + +describe('2026-09 ledger refresh', () => { + const ledger = projectsData.projects as Project[] + + it('shows Creator OS as a restricted external pilot with a public read-only demo, not a paid launch', () => { + expect(ledger).toHaveLength(48) + const creator = ledger.find((project) => project.slug === 'creator-learning-os') + expect(creator).toBeDefined() + expect(creator?.status).toBe('building') + expect(creator?.private).toBe(true) + expect(creator?.website).toBe('https://creator.writingdeveloper.blog/demo') + expect(creator?.descriptionEn).toContain('Testing-only') + expect(creator?.caseStudy?.evidenceEn).toContain('without a provider model call') + }) + + it('adds BRAIN//VAT as a public demo without exposing a private repo', () => { + const bvat = ledger.find((project) => project.slug === 'brain-vat-siege') + expect(bvat).toBeDefined() + expect(bvat?.website).toBe('https://brain-vat-siege.vercel.app') + expect(bvat?.private).toBe(true) + expect(bvat?.github).toBeUndefined() + expect(bvat?.screenshot).toBe('/images/projects/brain-vat-siege.webp') + expect(bvat?.caseStudy?.evidenceEn).toContain('66/90') + }) it('keeps JuiceBar in the expanded portfolio ledger', () => { - expect(ledger).toHaveLength(43) + expect(ledger).toHaveLength(48) const juicebar = ledger.find((project) => project.slug === 'juicebar') expect(juicebar).toBeDefined() expect(juicebar?.github).toBe('https://github.com/writingdeveloper/JuiceBar') diff --git a/src/lib/evidence-policy.ts b/src/lib/evidence-policy.ts new file mode 100644 index 0000000..64c3d0d --- /dev/null +++ b/src/lib/evidence-policy.ts @@ -0,0 +1,98 @@ +export interface EvidenceRecord { + id: string + project: string + kind: string + snapshotAt: string + reviewedAt: string + titleEn: string + titleKo: string + value: string + measurements: Record + scopeEn: string + scopeKo: string + source: { repository: string; path: string; commit: string; sha256: string } +} +export interface EvidenceManifest { + schemaVersion: number + reviewAfter: string + records: EvidenceRecord[] +} + +// A repository/filename allowlist prevents the collector from reading arbitrary private files. +export const EVIDENCE_SOURCES: Record = { + 'devdeck-maintenance': { + repository: 'writingdeveloper/devdeck', + path: 'docs/quality-review-2026-09-08.md', + localDirectory: 'devdeck', + }, + 'citefirst-retrieval': { repository: 'writingdeveloper/citefirst', path: 'README.md', localDirectory: 'citefirst' }, +} +const validDate = (value: string) => + /^\d{4}-\d{2}-\d{2}$/.test(value) && + !Number.isNaN(Date.parse(value)) && + new Date(value).toISOString().slice(0, 10) === value + +export function evidenceProblems(manifest: EvidenceManifest, now = new Date()): string[] { + const issues: string[] = [] + if (manifest.schemaVersion !== 1) issues.push('Unsupported evidence schema') + if (!validDate(manifest.reviewAfter)) issues.push('Invalid reviewAfter') + const seen = new Set() + for (const record of manifest.records) { + if (seen.has(record.id)) issues.push('Duplicate evidence: ' + record.id) + seen.add(record.id) + const source = EVIDENCE_SOURCES[record.id] + if (!source || source.repository !== record.source.repository || source.path !== record.source.path) + issues.push('Source not allowlisted: ' + record.id) + const m = record.measurements + const nonnegativeInt = (v: number | undefined) => typeof v === 'number' && Number.isInteger(v) && v >= 0 + const fraction = (v: number | undefined) => typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 1 + const validMeasurements = + Boolean(m) && + (record.id === 'devdeck-maintenance' + ? nonnegativeInt(m.passed) && nonnegativeInt(m.skipped) + : record.id === 'citefirst-retrieval' + ? nonnegativeInt(m.questions) && m.questions! > 0 && fraction(m.recall5) && fraction(m.mrr10) + : false) + if (!validMeasurements) issues.push('Invalid measurements') + else if (evidenceValue(record) !== record.value) issues.push('Display value differs from measurements') + if (record.kind !== 'source-snapshot') issues.push('Only source snapshots may be published here') + if (!/^[a-f0-9]{40}$/.test(record.source.commit)) issues.push('Source must use a full commit SHA') + if (!/^[a-f0-9]{64}$/.test(record.source.sha256)) issues.push('Invalid source digest') + for (const date of [record.snapshotAt, record.reviewedAt]) + if (!validDate(date) || Date.parse(date) > now.getTime()) issues.push('Invalid/future evidence date') + if (record.snapshotAt > record.reviewedAt) issues.push('Review predates snapshot') + if (record.reviewedAt > manifest.reviewAfter) issues.push('Review deadline predates verification') + for (const text of [record.titleEn, record.titleKo, record.scopeEn, record.scopeKo, record.value]) + if (!text?.trim()) issues.push('Missing localized evidence/scope') + } + if (manifest.records.length === 0) issues.push('Missing evidence records') + return issues +} +export function evidenceUrl(record: EvidenceRecord): string { + return 'https://github.com/' + record.source.repository + '/blob/' + record.source.commit + '/' + record.source.path +} + +/** These narrow parsers fail when upstream formats change instead of inventing a new count. */ +export function extractMeasurements(id: string, text: string): Record { + if (id === 'devdeck-maintenance') { + const match = text.match(/Unit suite including[^|]*\|\s*\d+ files; ([\d,]+) passed, (\d+) pre-existing skip/) + if (!match) throw new Error('DevDeck validation row not recognized') + return { passed: Number(match[1].replaceAll(',', '')), skipped: Number(match[2]) } + } + if (id === 'citefirst-retrieval') { + const questions = text.match(/(\d+) questions, in the shipped configuration/) + const recall = text.match(/^\| recall@5 \|[^\n]*?\*\*([\d.]+)\*\*/m) + const mrr = text.match(/^\| MRR@10 \|[^\n]*?\*\*([\d.]+)\*\*/m) + if (!questions || !recall || !mrr) throw new Error('citefirst evaluation table not recognized') + return { questions: Number(questions[1]), recall5: Number(recall[1]), mrr10: Number(mrr[1]) } + } + throw new Error('Unapproved evidence source: ' + id) +} +export function evidenceValue(record: EvidenceRecord): string { + const m = record.measurements + if (record.id === 'devdeck-maintenance') + return m.passed!.toLocaleString('en-US') + ' passed · ' + m.skipped + ' skipped' + if (record.id === 'citefirst-retrieval') + return 'recall@5 ' + m.recall5!.toFixed(3) + ' · MRR@10 ' + m.mrr10!.toFixed(3) + throw new Error('Unknown evidence kind') +} diff --git a/src/lib/projects.ts b/src/lib/projects.ts index 4cb246a..c249b2c 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -1,6 +1,16 @@ import type { Project } from '@/types/content' -export const SELECTED_WORK = ['devdeck', 'studios', 'rentrights', 'citefirst', 'juicebar'] as const +/** Concise catalogue copy; full detail remains on project-specific pages. */ +export function projectExcerpt(description: string, maxLength = 220): string { + const text = description.trim().replace(/\s+/g, ' ') + if (text.length <= maxLength) return text + const limit = text.slice(0, maxLength) + const boundary = limit.lastIndexOf(' ') + const cut = boundary >= maxLength * 0.6 ? boundary : maxLength + return limit.slice(0, cut).trimEnd() + '…' +} + +export const SELECTED_WORK = ['devdeck', 'studios', 'citefirst', 'brain-vat-siege', 'healframe'] as const export type ProjectCaseStudyKey = 'problem' | 'decision' | 'evidence' | 'result' @@ -192,6 +202,9 @@ export const HOME_GROUPS: Record<'products' | 'games' | 'tools', string[]> = { 'observer-of-lines', ], games: [ + 'brain-vat-siege', + 'railforge', + 'project-strata', 'coinrace', 'wishing-stones', 'hoverslam', @@ -204,10 +217,12 @@ export const HOME_GROUPS: Record<'products' | 'games' | 'tools', string[]> = { 'studio-apartment', ], tools: [ - 'citefirst', - 'juicebar', 'devdeck', 'studios', + 'music-mv-studio', + 'creator-learning-os', + 'citefirst', + 'juicebar', 'til-shorts', 'mv-analyzer', 'sitedeck', diff --git a/src/lib/skill-groups.ts b/src/lib/skill-groups.ts new file mode 100644 index 0000000..5d36711 --- /dev/null +++ b/src/lib/skill-groups.ts @@ -0,0 +1,8 @@ +import { skillCategories, type Skill } from '@/types/content' + +/** Shared by the standard pages and the 3D scene: categories cannot silently disappear. */ +export function groupSkills(skills: Skill[]): Record { + return Object.fromEntries( + Object.keys(skillCategories).map((category) => [category, skills.filter((skill) => skill.category === category)]) + ) as Record +} diff --git a/src/lib/telemetry-policy.ts b/src/lib/telemetry-policy.ts new file mode 100644 index 0000000..577f2b2 --- /dev/null +++ b/src/lib/telemetry-policy.ts @@ -0,0 +1,11 @@ +export interface TelemetryEnvironment { + VERCEL_ENV?: string + PORTFOLIO_ENABLE_TELEMETRY?: string +} + +/** Local/previews must not request Vercel-only endpoints or pollute real visitor analytics. */ +export function telemetryPolicy(env: TelemetryEnvironment, host: string | null) { + const canonicalHost = host?.toLowerCase().replace(/:443$/, '') === 'writingdeveloper.blog' + const publicProduction = canonicalHost && (env.VERCEL_ENV === 'production' || env.PORTFOLIO_ENABLE_TELEMETRY === '1') + return { thirdParty: publicProduction, vercel: publicProduction && env.VERCEL_ENV === 'production' } +} diff --git a/src/types/content.ts b/src/types/content.ts index 7e9af7a..7fc7081 100644 --- a/src/types/content.ts +++ b/src/types/content.ts @@ -65,7 +65,7 @@ export interface Tombstone { export interface Skill { name: string - category: 'frontend' | 'backend' | 'tools' | 'infra' + category: 'ai' | 'frontend' | 'backend' | 'tools' | 'infra' } export interface TimelineItem { @@ -88,6 +88,7 @@ export interface PostSummary { } export const skillCategories = { + ai: { ko: 'AI / 에이전트 시스템', en: 'AI / Agent Systems' }, frontend: { ko: '프론트엔드', en: 'Frontend' }, backend: { ko: '백엔드', en: 'Backend' }, tools: { ko: '도구', en: 'Tools' },