feat(resume-draft): 経歴書ドラフトを project 明細へ縮小し候補選択制にする(#563〜#567) - #584
Conversation
ADR-0026 決定 3 / #562。 - select_repos の主キーを「最終 push 日時」から「継続期間 × 実装量」へ変更し、 直近性はタイブレークへ降格した。最終 tie-breaker は full_name 辞書順のまま 維持し、同一入力から常に同一の並びが出る完全順序を不変条件とした - 候補を落とさず順位付けだけを行う rank_repos を新設し、select_repos はその 上位を PROJECT_LIMIT で打ち切る薄いラッパーにした(ADR-0026 決定 2 の 「機械は候補を落とさない」を #564 の候補一覧 API で使えるようにするため) - デフォルト非選択の判定 evaluate_default_selection を追加した。継続期間が 閾値未満・topics に学習用途語を含む場合に理由コードを返す。判定結果は デフォルト選択状態と理由表示にのみ影響し、候補一覧からは除外しない - スコア式・重み・閾値・topics 正規化規則の正本を mapper.py の定数に置いた (ADR に数値を複製しない)。float の丸めで同点判定がブレないよう整数演算に限定 - mutmut の only_mutate に mapper.py を追加した Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McjCjUC3j8HWfzGbLfMhyJ
ADR-0026 決定 1 / #563。 BREAKING CHANGE: GET /api/agent/resume-draft/result のレスポンスから experiences / qualifications を削除し、projects(プロジェクト明細のリスト)に 置き換えた。web のフォーム注入は #566 で追随する。 - PLACEHOLDER_COMPANY / PLACEHOLDER_BUSINESS_DESCRIPTION / PLACEHOLDER_ROLE と チーム構成の固定値生成を削除した。GitHub から得られない値(会社・事業内容・ 役割・担当工程・チーム規模)は機械が埋めず空のまま人間が書く - build_skeleton の experience 生成を廃止し、出力をプロジェクト明細のリストへ 変更した。career_summary / self_pr は projects から独立した候補として返す - PDF レンダリング時のみ build_pdf_payload で Resume 互換の形へ包む。包む experience / client は空(会社名・顧客名はプレースホルダを入れない)とし、 projects が 0 件なら空箱すら作らない - agent_resume_draft.md に「役割・担当工程・チーム規模を書かない」を明記した - make codegen-types の生成差分(web/src/api/generated.ts)を同梱 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McjCjUC3j8HWfzGbLfMhyJ
ADR-0026 決定 2 / #564。 BREAKING CHANGE: POST /api/agent/resume-draft/run に repo_full_names(1 件以上・ 上限 5 件)が必須になった。web の導線は #565 で追随する。 - GET /api/agent/resume-draft/candidates を追加した。入口フィルタを通った リポジトリを全件返し、シグナル(継続期間・実装量・技術スタック・IaC 有無)と デフォルト選択状態・非選択理由コードを添える。機械は候補を落とさない - ドラフト生成は採用リポジトリの指定を必須にし、LLM の説明文生成を採用分のみに 限定した(コストが選択数に比例する / P1)。出力スキーマの enum も採用分で縛る - PROJECT_LIMIT の意味を「LLM 出力の有界化」から「1 回に選べる上限」へ変更し、 正本を schemas/agent.py の RESUME_DRAFT_SELECTION_LIMIT へ移した(mapper が import する。schemas → services の循環 import を避けるため) - 選択 0 件・上限超過はスキーマ検証で 422、連携データに無いリポジトリ指定は router で 422 にして捏造リポの構造混入を入口で止める。タスク payload 側でも 採用リポジトリ欠落・未知リポジトリを NonRetryableError で二重ガードする - 非同期タスク構造・冪等ガード(ADR-0020)は変更していない - make codegen-types の生成差分を同梱 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McjCjUC3j8HWfzGbLfMhyJ
ADR-0026 決定 2 / 3 / 5 / #565・#566。 #563 のレスポンス契約変更(experiences → projects)に web を追随させる変更で、 両 issue を分けると web の型チェックが通らない期間ができるため 1 コミットにまとめた。 #565 リポジトリ候補の選択 UI: - GET /resume-draft/candidates を叩く useResumeDraftCandidates と、候補一覧 コンポーネント ResumeDraftCandidateList を追加した。シグナル(継続期間・実装量・ 技術スタック・IaC 有無)を表示し、デフォルト非選択には理由バッジを出す - デフォルト非選択の候補もユーザーが選び直せる(機械の判定を常に覆せる) - 上限超過時は未選択のチェックボックスだけを無効化し、選択解除は妨げない - 選択 0 件では生成ボタンを押せないようにした - 連携結果がまだ無い画面では候補を取得しない(必ず 409 になる要求で エラートーストを出さないため。E2E で検知した) #566 ドラフト注入の「置換」→「追加」: - applyResumeDraftToForm(experiences 丸ごと置換)を廃止し、 appendResumeDraftProjects を追加した。ユーザーが指定した experience / client の projects へ追加するだけで、既存の職歴は書き換えない - 追加先はユーザーの明示指定を必須にし、機械は推測しない。experience が 1 件も無い場合のみ空の experience / client を 1 件ずつ作る - 同一 client 内に同名 project があれば追加しない(冪等。判定キーは project 名) - 部分適用しない(追加先が不正なら例外を投げ、フォーム state は変更しない) - career_summary / self_pr は上書きせず候補として提示し、ユーザーが個別に適用する - 追加のみになったため上書き確認ダイアログを廃止した - ResumeFormSource から ResumeDraftResultResponse を外した(保存済み経歴書専用へ) - 文言はすべて constants/messages.ts に追加した E2E(resume-draft-apply / github-link)を新しい導線に合わせて更新し、全 39 件 green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McjCjUC3j8HWfzGbLfMhyJ
#567。 - .claude/rules/backend/agent.md の resume_draft 節を新設計へ更新した。 出力単位(experience → project)・プレースホルダ生成の廃止・選定が 「候補提示 + 人間の採用」であること・LLM は採用分のみ実行することを 「崩してはいけない不変条件」として明記した - .claude/rules/backend/architecture.md の resume_draft/ 構成図を更新した (mapper.py の責務追記・run_task.py の追加) - docs/api.md にドラフト生成の節を新設し、候補一覧 API を含む 5 エンドポイントの 契約(返す単位・バリデーション・409/422 の条件)を記載した - ADR-0026 の関連リンクに実装状況を追記し、「ADR では固定しない」とした スコア式・閾値・語彙・上限の正本(mapper.py / schemas/agent.py)への 参照表を足した(数値は複製せず drift を防ぐ) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McjCjUC3j8HWfzGbLfMhyJ
📝 WalkthroughWalkthroughThe resume draft workflow now supports repository candidate discovery, user-selected generation, project-only results, PDF compatibility conversion, and targeted project injection into existing career records. ChangesResume draft backend
Frontend selection and injection
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
backend/tests/test_resume_draft_mapper.py (1)
289-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBuild the boundary dates with date arithmetic.
The test formats the day number directly from
MIN_DURATION_DAYS. IfMIN_DURATION_DAYSbecomes 31 or larger, the string becomes an invalid date such as2026-01-32._parse_datethen returnsNone,duration_daysreturns 0, and the test fails for a date-format reason instead of the boundary it verifies. Derive the pushed date from atimedeltaso the test stays valid for any threshold value.♻️ Proposed refactor
def test_default_selection_duration_threshold_boundary() -> None: """閾値ちょうどは選択、1 日足りなければ非選択(境界の固定)。""" + created = date(2026, 1, 1) exactly = _repo( - "o/exact", created="2026-01-01T00:00:00Z",- pushed=f"2026-01-{1 + MIN_DURATION_DAYS:02d}T00:00:00Z",+ "o/exact", created=f"{created.isoformat()}T00:00:00Z",+ pushed=f"{(created + timedelta(days=MIN_DURATION_DAYS)).isoformat()}T00:00:00Z", ) one_short = _repo( - "o/short", created="2026-01-01T00:00:00Z",- pushed=f"2026-01-{MIN_DURATION_DAYS:02d}T00:00:00Z",+ "o/short", created=f"{created.isoformat()}T00:00:00Z",+ pushed=f"{(created + timedelta(days=MIN_DURATION_DAYS - 1)).isoformat()}T00:00:00Z", )Add the import:
-from datetime import date+from datetime import date, timedelta🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/tests/test_resume_draft_mapper.py` around lines 289 - 300, Update test_default_selection_duration_threshold_boundary to construct both pushed timestamps using date arithmetic with timedelta from the fixed created date, rather than interpolating day numbers from MIN_DURATION_DAYS. Add the required timedelta import and preserve the exact-threshold and one-day-short assertions.backend/app/services/agent/resume_draft/mapper.py (1)
157-173: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unused
select_reposand update test callers.
select_reposis only called by tests; production code now usesbuild_candidatesorselect_requested_repos. Drop the public function and replace thebuild_skeleton(select_repos(...))test inputs withrank_repos(source)[:PROJECT_LIMIT].🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/app/services/agent/resume_draft/mapper.py` around lines 157 - 173, Remove the unused public select_repos function and update all test callers that invoke build_skeleton(select_repos(...)) to use rank_repos(source)[:PROJECT_LIMIT] instead. Keep rank_repos unchanged and preserve the existing limit applied to test inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/rules/backend/architecture.md:
- Around line 64-69: Update the task-handler registration documentation and
related registry comment to list both TaskType.GITHUB_LINK and
TaskType.RESUME_DRAFT, reflecting the registered ResumeDraftHandler. Replace the
stale GITHUB_LINK-only wording in dispatch_service.py and the architecture
documentation while preserving the existing descriptions.
In `@web/src/components/forms/ResumeDraftInjectDialog.tsx`:
- Around line 73-116: Update the experience-selection and append flow around
ResumeDraftInjectDialog so an experience with no clients cannot be submitted
with the default clientIndex of 0. Disable the append action and show an
explicit unavailable-target message, or otherwise filter those experiences
before selection; preserve valid appending for experiences with at least one
client and ensure appendResumeDraftProjects is never called with an invalid
client target.
In `@web/src/components/github-link/GitHubLinkDashboard.tsx`:
- Around line 74-77: Update the candidate-loading flow around
useResumeDraftCandidates and the runLink/polling lifecycle so candidates are
refreshed when the latest GitHub link result changes, including when result
remains non-null across successful links. Ensure previous-analysis candidates
are loaded during polling as needed, then trigger a reload using a
result-derived key or equivalent change signal once the new result arrives.
In `@web/src/formMappers.ts`:
- Around line 12-21: Update ResumeFormSource in web/src/formMappers.ts (lines
12-21) to omit id along with created_at and updated_at, allowing id-less
pre-save payloads. In web/src/formMappers.test.ts (lines 39-60), remove the id
property and the broad unknown cast so the test validates the intended type
directly.
---
Nitpick comments:
In `@backend/app/services/agent/resume_draft/mapper.py`:
- Around line 157-173: Remove the unused public select_repos function and update
all test callers that invoke build_skeleton(select_repos(...)) to use
rank_repos(source)[:PROJECT_LIMIT] instead. Keep rank_repos unchanged and
preserve the existing limit applied to test inputs.
In `@backend/tests/test_resume_draft_mapper.py`:
- Around line 289-300: Update test_default_selection_duration_threshold_boundary
to construct both pushed timestamps using date arithmetic with timedelta from
the fixed created date, rather than interpolating day numbers from
MIN_DURATION_DAYS. Add the required timedelta import and preserve the
exact-threshold and one-day-short assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 12b9761d-a28f-4e43-bf55-391e5d87cab8
📒 Files selected for processing (40)
.claude/rules/backend/agent.md.claude/rules/backend/architecture.mdbackend/app/messages.jsonbackend/app/prompts/agent_resume_draft.mdbackend/app/routers/agent.pybackend/app/schemas/agent.pybackend/app/services/agent/resume_draft/draft_service.pybackend/app/services/agent/resume_draft/mapper.pybackend/app/services/agent/resume_draft/run_task.pybackend/pyproject.tomlbackend/tests/test_resume_draft_api.pybackend/tests/test_resume_draft_mapper.pybackend/tests/test_resume_draft_service.pybackend/tests/test_worker/test_resume_draft.pydocs/adr/0026-resume-draft-project-scope.mddocs/api.mdweb/e2e/github-link.spec.tsweb/e2e/resume-draft-apply.spec.tsweb/src/api/agent.tsweb/src/api/generated.tsweb/src/api/paths.tsweb/src/api/types.tsweb/src/components/forms/CareerResumeForm.tsxweb/src/components/forms/ResumeDraftInjectDialog.module.cssweb/src/components/forms/ResumeDraftInjectDialog.tsxweb/src/components/github-link/GitHubLinkDashboard.tsxweb/src/components/github-link/ResumeDraftCandidateList.module.cssweb/src/components/github-link/ResumeDraftCandidateList.test.tsxweb/src/components/github-link/ResumeDraftCandidateList.tsxweb/src/constants/messages.tsweb/src/formMappers.test.tsweb/src/formMappers.tsweb/src/hooks/useResumeDraftCandidates.test.tsweb/src/hooks/useResumeDraftCandidates.tsweb/src/hooks/useResumeDraftPdf.test.tsweb/src/hooks/useResumeDraftPdf.tsweb/src/utils/resumeDraftCandidates.test.tsweb/src/utils/resumeDraftCandidates.tsweb/src/utils/resumeImport.test.tsweb/src/utils/resumeImport.ts
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
PR #584 のレビュー指摘 6 件(Actionable 4 + Nitpick 2)を実コードで検証し、 いずれも再現したため修正した。 - 再連携で候補が古いままになる不具合を修正した(Functional Correctness)。 useAsyncTaskPage は transitionToPolling で result をクリアしないため、 連携済みユーザーが再連携しても Boolean(result) は true→true で変化せず、 候補の再取得が走らなかった。連携結果の分析時刻(analyzed_at)を鍵にして 取り直すようにし、旧挙動で落ちる回帰テストを追加した - 追加先の職歴に取引先が 1 件も無い場合、追加ボタンが押せて DRAFT_TARGET_OUT_OF_RANGE を投げていた。ボタンを無効化して理由を表示する (追加先はユーザーが明示指定する契約のため、機械が取引先を作らない) - ResumeFormSource から id を除外した。マッパーは id を参照しないのに必須 だったため、型の説明と実際が食い違い、テストがキャストで隠していた。 キャストを外して型どおり検証されるようにした - 非同期タスクが GITHUB_LINK の 1 種類のみという陳腐化した記述を修正した (dispatch_service.py / architecture.md 2 箇所)。RESUME_DRAFT は ADR-0020 で ResumeDraftHandler として登録済み。ハンドラ構成図にも追記 - 本番から呼ばれなくなった select_repos を削除し、テストの呼び出しを rank_repos(source)[:PROJECT_LIMIT] へ置き換えた(ミューテーション対象 ファイルの dead code を残さない) - 継続期間の境界テストを timedelta による日付演算へ変更した (MIN_DURATION_DAYS が 31 以上になると不正な日付文字列になり、境界とは 無関係な理由で落ちるため) backend 668 tests / web 383 tests / E2E 39 件 green。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McjCjUC3j8HWfzGbLfMhyJ
#585(PR #581 相当)が main に squash マージされ、同じ #562 の実装が両側に 別コミットとして入ったためコンフリクトしていた。 - mapper.py / test_resume_draft_mapper.py: 本ブランチ側を採用した。main 側の select_repos / evaluate_noise は本ブランチの rank_repos / evaluate_default_selection / build_candidates に置き換わっており、候補一覧 API・ 選択 UI がこちらの API に依存しているため - ただし main 側にしか無かった修正(継続期間の時刻成分切り捨て / PR #581 の CodeRabbit 指摘)は本ブランチの duration_days へ移植した。_parse_datetime を 追加して UTC の aware datetime で減算する。日粒度でよい「参画中」判定は _parse_date のまま残す - 移植分のテスト 2 件(閾値直下 29 日 1 分の境界 / タイムゾーン混在表記)を 本ブランチの evaluate_default_selection の契約に合わせて追加した - pyproject.toml: only_mutate の同一エントリで、コメント文言のみ本ブランチ側を採用 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KaqnCny4wsD6AkBkJeTEEV
Uh oh!
There was an error while loading. Please reload this page.
ADR-0026 の実装。マイルストーン「ドラフト縮小 N/7」のうち #563 / #564 / #565 / #566 / #567 を依存順に実装する。
変更内容
#563 出力を project 明細へ縮小しプレースホルダ生成を廃止(決定 1)
破壊的変更:
GET /api/agent/resume-draft/resultのレスポンスからexperiences/qualificationsを削除し、projectsに置き換えた。PLACEHOLDER_COMPANY/PLACEHOLDER_BUSINESS_DESCRIPTION/PLACEHOLDER_ROLEとチーム構成の固定値生成を削除。GitHub から得られない値(会社・事業内容・役割・担当工程・チーム規模)は機械が埋めず空のまま人間が書くbuild_skeletonの experience 生成を廃止し、出力をプロジェクト明細のリストへ変更。career_summary/self_prは projects から独立した候補として返すbuild_pdf_payloadで Resume 互換の形へ包む。包む experience / client は空(プレースホルダを入れない)で、projects が 0 件なら空箱すら作らない#564 候補一覧 API と選択付きドラフト生成(決定 2)
破壊的変更:
POST /api/agent/resume-draft/runにrepo_full_names(1 件以上・上限 5 件)が必須になった。GET /api/agent/resume-draft/candidatesを追加。入口フィルタを通ったリポジトリを全件返し、シグナル(継続期間・実装量・技術スタック・IaC 有無)とデフォルト選択状態・非選択理由コードを添える。機械は候補を落とさないPROJECT_LIMITの意味を「LLM 出力の有界化」から「1 回に選べる上限」へ変更し、正本をschemas/agent.pyのRESUME_DRAFT_SELECTION_LIMITへ移した#565 リポジトリ候補の選択 UI(決定 2 / 3)
#566 ドラフト注入を「置換」から「追加」へ(決定 5)
applyResumeDraftToForm(experiences 丸ごと置換)を廃止し、appendResumeDraftProjectsを追加。ユーザーが指定した experience / client のprojectsへ追加するだけで、既存の職歴は書き換えないcareer_summary/self_prは上書きせず候補として提示し、ユーザーが個別に適用する#567 rules / docs への同期
.claude/rules/backend/agent.mdのresume_draft節を新設計へ更新(出力単位・プレースホルダ廃止・候補提示 + 人間の採用・LLM は採用分のみを不変条件として明記).claude/rules/backend/architecture.mdの構成図を更新docs/api.mdにドラフト生成の節を新設し、候補一覧 API を含む 5 エンドポイントの契約を記載検証
make ci相当をすべて green(backend 670 tests / web 383 tests / E2E 39 件)。lint・codegen-drift・lint-tdd・lint-adr-index も通過。TDD 対象(
mapper.py/utils//hooks/)は red の失敗出力を確認してから実装している。レビュー対応
CodeRabbit の指摘 6 件(Actionable 4 + Nitpick 2)は全件を実コードで検証し、
c01a7baで対応済み。うち 2 件は実バグだった(再連携後に候補が古いまま残る/取引先ゼロの職歴へ追加できてしまう)。