careermate 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/AGENTS.md +5 -3
  2. package/dist/career-os/README.md +4 -3
  3. package/dist/career-os/eop/career-description.md +3 -0
  4. package/dist/career-os/eop/cover-letter.md +4 -1
  5. package/dist/career-os/eop/fit-analysis.md +1 -1
  6. package/dist/career-os/eop/job-analysis.md +1 -1
  7. package/dist/career-os/knowledge/cover-letter.md +1 -0
  8. package/dist/career-os/knowledge/fit-matching.md +3 -3
  9. package/dist/career-os/knowledge/human-writing.md +2 -0
  10. package/dist/career-os/knowledge/verifiers/ats-compat.md +1 -1
  11. package/dist/career-os/knowledge/verifiers/human-voice.md +2 -0
  12. package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +1 -1
  13. package/dist/doctor.mjs +20 -11
  14. package/dist/mcp.mjs +21 -12
  15. package/dist/web.mjs +2 -1
  16. package/docs/MCP_TOOLS.md +5 -3
  17. package/docs/START_WORKFLOW.md +1 -1
  18. package/docs/career-os/README.md +4 -3
  19. package/docs/career-os/eop/career-description.md +3 -0
  20. package/docs/career-os/eop/cover-letter.md +4 -1
  21. package/docs/career-os/eop/fit-analysis.md +1 -1
  22. package/docs/career-os/eop/job-analysis.md +1 -1
  23. package/docs/career-os/knowledge/cover-letter.md +1 -0
  24. package/docs/career-os/knowledge/fit-matching.md +3 -3
  25. package/docs/career-os/knowledge/human-writing.md +2 -0
  26. package/docs/career-os/knowledge/verifiers/ats-compat.md +1 -1
  27. package/docs/career-os/knowledge/verifiers/human-voice.md +2 -0
  28. package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +1 -1
  29. package/package.json +2 -2
  30. package/packages/knowledge/src/index.ts +10 -3
  31. package/packages/mcp-tools/src/tools.ts +8 -6
  32. package/packages/prompts/src/system.ts +1 -0
package/AGENTS.md CHANGED
@@ -12,16 +12,18 @@ CareerMate는 사용자 PC에만 데이터를 두는 **로컬 커리어 작업
12
12
 
13
13
  - **사용자의 언어로 응답하라(출력 언어).** 기본값은 사용자가 쓴 언어다 — 한국어로 말하면 한국어로, 영어 등 다른 언어로 말하면 그 언어로 답한다. 이 지침·도구 응답(`user_message` 등)·플레이북이 한국어로 쓰여 있어도 그대로 복사하지 말고 **사용자 언어로 옮겨** 전달하고, 선택지·예시·안내 문구도 같은 언어로 맞춘다. 단 **사실·수치·고유명사·저장된 원문은 언어와 무관하게 보존**한다. (대화 언어와 *제출용 산출물의 언어·시장 관행*은 분리한다 — 자소서·이력서 본문의 언어와 관행은 **대상 공고·시장**을 따르며, 한국 공고면 한국 관행(문항형·NCS·블라인드), 비한국 공고면 그 시장 관행(ATS·단일 레터)을 적용한다. 대화 언어와 다를 수 있다.)
14
14
  - **기존 데이터를 항상 먼저 활용하라(조회는 최소로).** 연결 직후에는 `get_onboarding_status` **하나만** 부른다 — 무엇이 비었는지·완성도·다음 단계를 한 번에 돌려준다. 완전 신규(프로필 없음)면 더 조회하지 말고 곧장 사용자에게 이력서·정보를 청해 저장 단계로 간다. 이미 채워진 게 있을 때만 `get_application_context`를 **한 번** 더 불러 본문(프로필·대표 이력서·경력·프로젝트·스킬·자소서·최근 지원)을 가져온다 — `list_jobs`·`get_profile`·`get_resumes`·`get_cover_letters`·`list_recent_activity`는 거기에 이미 포함되니 온보딩에서 따로 부르지 마라(불필요한 호출은 사용자 승인 프롬프트만 늘린다). 이미 채워진 항목은 다시 묻지 마라.
15
- - **사용자가 파일을 주면 `read_document`로 읽어라.** 이력서·경력기술서·자소서·채용공고가 PDF·Word(`.docx`)·한컴(`.hwp`/`.hwpx`)·텍스트(`.txt`/`.md`) 파일로 컴퓨터에 있고 사용자가 경로로 가리키면, `read_document({ path })`로 본문 텍스트를 추출한 뒤 그 내용으로 작업하고 결과는 아래 저장 도구로 보관한다(파일 내용을 새 파일로 다시 쓰지 마라). 경로는 가능하면 절대 경로로 준다. 이미지·스캔 PDF는 텍스트 레이어가 없어 추출되지 않을 수 있는데, 그때는 이미지를 직접 볼 수 있는 클라이언트로 읽거나 사용자에게 내용을 텍스트로 붙여넣어 달라고 안내한다.
15
+ - **사용자가 파일을 주면 `read_document`로 읽어라.** 이력서·경력기술서·자소서·채용공고가 PDF·Word(`.docx`)·한컴(`.hwp`/`.hwpx`)·텍스트(`.txt`/`.md`) 파일로 컴퓨터에 있고 사용자가 경로로 가리키면, `read_document({ path })`로 본문 텍스트를 추출한 뒤 그 내용으로 작업하고 결과는 아래 저장 도구로 보관한다(파일 내용을 새 파일로 다시 쓰지 마라). 경로는 가능하면 절대 경로로 준다. 이미지·스캔 PDF는 텍스트 레이어가 없어 추출되지 않을 수 있는데, 그때는 이미지를 직접 볼 수 있는 클라이언트로 읽거나 사용자에게 내용을 텍스트로 붙여넣어 달라고 안내한다. **여러 문서를 한꺼번에 넘기려 하거나 파일 첨부가 안 되는 클라이언트(CLI 등)면**, `open_inbox`로 인입 폴더(`~/.careermate/inbox`)를 안내한 뒤 사용자가 다 넣었다고 하면 `read_inbox`로 한 번에 본문을 추출한다(낱개 경로를 직접 알려주는 경우엔 `read_document`). `read_inbox`도 `read_document`와 동일하게 PDF·Word·HWP를 자동 추출하고, 텍스트로 추출되지 않는 파일(텍스트 레이어 없는 이미지·스캔본 등)만 직접 읽기로 넘긴다. **공고를 파일이 아니라 URL(링크)로 주면 CareerMate는 그 페이지를 가져오지 않는다 — 네(AI)의 브라우징·웹 도구로 본문을 직접 읽어 분석하고 `save_job_posting`에 텍스트로 저장하라(데이터 사고·수집은 두뇌인 당신 몫이다). 웹 도구가 없는 환경이면 사용자에게 공고 본문을 붙여넣어 달라고 요청한다.**
16
16
  - **공고 분석·자소서 작성 전에는 반드시 `get_application_context`를 먼저 호출하라.** 한 번의 호출로 프로필·대표 이력서·경력·스킬·기존 자소서·대상 공고·선호 문체·강조 포인트를 모두 돌려준다. 맥락 없이 작성하지 마라.
17
- - **결과는 다시 CareerMate에 저장하라.** 분석 → `save_fit_analysis`, 자소서 → `save_cover_letter_version`, 경력기술서 → `add_resume`(`kind=career_description`), 공고 → `save_job_posting`, 면접 자료 → `save_interview_prep`. 커리어 데이터는 흩어진 파일이 아니라 CareerMate 안에 둔다. 임의의 로컬 파일 생성은 최소화하라(사용자가 파일을 원하면 `export_cover_letter`로 `~/.careermate/exports`에 내보낸다).
17
+ - **결과는 다시 CareerMate에 저장하라.** 분석 → `save_fit_analysis`, 자소서 → `save_cover_letter_version`, 경력기술서 → `add_resume`(`kind=career_description`), 공고 → `save_job_posting`, 면접 자료 → `save_interview_prep`. 커리어 데이터는 흩어진 파일이 아니라 CareerMate 안에 둔다. 임의의 로컬 파일 생성은 최소화하라(사용자가 파일을 원하면 자소서는 `export_cover_letter`, 이력서·경력기술서는 `export_resume`, 프로필은 `export_profile`로 `~/.careermate/exports`에 내보낸다).
18
+ - **여러 문서를 한 번에 요청받아도 각 문서는 각자의 저장 도구로 따로 저장하라.** "경력기술서랑 자기소개서 둘 다 써줘"처럼 한 턴에 여러 문서를 만들었더라도, 한 저장 호출의 본문(content)에 다른 문서를 이어붙이지 마라 — 경력기술서는 `add_resume`(`kind=career_description`), 자기소개서는 `save_cover_letter_version`으로 **각각 따로** 저장한다(작성은 한 답변에 둘 다 보여줘도 무방하나, 저장은 문서별 도구로 분리). 한 문서 본문에 다른 문서가 섞이면 대시보드에 통째로 붙어 등록된다.
18
19
  - **도구 응답은 사람 말로 바꿔라.** 응답에 `user_message`가 있으면 그 내용을 우선 활용하되 **사용자의 언어로 옮겨** 전달하고(한국어 원문을 그대로 복사하지 않는다), `suggested_next_action`은 선택지로 제안한다. `id`, `job_id`, `cover_letter_id`, `workflow_id`, JSON 원문, 도구 이름은 내부 작업용으로만 쓰고 사용자에게 그대로 복사하지 않는다.
19
- - **공고 분석 뒤에는 자기소개서를 곧장 쓰지 말고 선택지를 보여줘라.** 적합도 분석을 저장하고 쉬운 말로 설명한 뒤 "이 공고에 맞춘 자기소개서를 써드릴까요?"라고 묻고, `1. 네, 이 공고에 맞춘 자기소개서를 써줘` / `2. 아니요, 분석만 저장해줘`처럼 고르게 한다. 사용자가 1번을 고른 경우에만 자소서 작성·저장 흐름으로 이어간다.
20
+ - **공고 분석 뒤에는 자기소개서를 곧장 쓰지 말고 선택지를 보여줘라.** 적합도 분석을 저장하고 쉬운 말로 설명한 뒤 "이 공고에 맞춘 자기소개서를 써드릴까요?"라고 묻고, `1. 네, 이 공고에 맞춘 자기소개서를 써줘` / `2. 아니요, 분석만 저장해줘`처럼 고르게 한다. 사용자가 1번을 고른 경우에만 자소서 작성·저장 흐름으로 이어간다. **1번(쓰기)을 고르면 작성 전에, 회사가 지정한 문항·양식·글자수 제한이 있으면 먼저 달라고 요청하라** — 한국 자소서는 문항형(회사별 항목·글자수)이 많아 문항·양식이 결과 질을 좌우한다. 공고·맥락에 없으면 사용자에게 문항 텍스트·제출 양식·글자수(상·하한, 공백포함 여부)를 묻고, 끝내 없으면 일반 양식으로 진행한다고 알린다. 받은 문항·양식·글자수는 작성에 반영하고 가능하면 공고에 함께 저장한다.
20
21
  - **표준 워크플로우를 따르라(핵심 6 + 선택 1)**: onboarding, analyze_job, write_cover_letter, write_career_description, manage_application_status, prepare_interview — 그리고 사용자가 LinkedIn·포트폴리오 같은 자기표현 자산을 요청하면 `build_personal_brand`. 순서가 헷갈리면 `get_workflow_guide`를 호출한다.
21
22
  - **사용자 대면 결과는 전문가 절차로 만들고, 저장 전 자가검증하라.** 공고 분석·자소서·경력기술서·면접 자료 등을 만들기 전에는 `get_workflow_guide`로 그 작업의 실행 절차(EOP)와 적용할 전문가 플레이북·검증기 순서를 먼저 받고, `get_playbook`(도메인 깊은 지식)으로 작성한 뒤 저장 직전 `get_verifier`(검증 루브릭)로 **당신이** 자가검증한다(사람이 쓴 듯한 글은 작성 직전 `get_writing_style_guide`도 함께 적용). CareerMate는 셀 수 있는 항목만 돕고 의미 판단은 당신 몫이다. 사실·수치·고유명사는 그대로 보존한다.
22
23
  - **8단계 지원 상태를 존중하라**: `draft`(작성 중) → `planned`(지원 예정) → `applied`(지원 완료) → `document_passed`(서류 합격) → `interview`(면접 진행) → `final_passed`(최종 합격) / `rejected`(불합격) / `on_hold`(보류). 면접 준비는 사용자가 요청하면 공고 기준으로 저장할 수 있고, `document_passed` 이상에서는 다음 행동으로 우선 제안한다. 상태 변경은 `update_application_status`.
23
24
  - **업데이트 요청은 구분해서 처리하라.** 사용자가 "최신이야?", "업데이트 있어?"처럼 확인만 요청하면 `check_for_update`로 확인한다. "업데이트해줘", "CareerMate 업데이트해줘", "최신으로 올려줘"처럼 적용을 명시하면 `update_careermate`를 바로 호출한다(이 도구가 최신 버전 확인도 함께 한다). 성공 후에는 AI 앱 또는 MCP 연결 재시작이 필요하다고 안내한다.
24
25
  - **결과를 낼 때마다 대시보드로 안내하라.** 공고 저장·적합도 분석·자소서 버전·상태 변경·면접 준비 등 무언가를 저장·완료할 때마다 대시보드(`http://127.0.0.1:4319`) 주소를 알려준다. 브라우저를 직접 여는 건 **첫 온보딩 때 한 번만**(`open_dashboard`) — 이후에는 자동으로 열지 말고 링크만 보여주고, 사용자가 "열어줘"라고 하면 그때 `open_application`(단건)·`open_dashboard`(전체)로 연다.
26
+ - **지원·문의·버그 신고는 정해진 경로로 안내하라(연락처를 지어내지 마라).** 사용자가 문제 신고·기능 요청·일반 문의를 하면: **버그·기능 요청**은 GitHub Issues(`https://github.com/osntak/careermate/issues`), **일반 문의**는 공개 이메일(`osntak@gmail.com`)로 안내한다 — 같은 링크가 **대시보드 설정 화면의 '도움말·문제 신고' 카드**(영어 UI는 'Help & report a problem')와 **사이트 푸터(careermate.life)** 에도 모여 있으니 그쪽을 알려줘도 된다. **보안 취약점은 공개 이슈로 올리지 말고** 비공개 절차(`SECURITY.md`의 GitHub Security Advisories)로 안내한다. 어느 경로든 신고·문의에 **이름·이메일·연락처·자소서 본문 같은 개인정보를 붙여넣지 말라고 먼저 경고**한다(데이터는 로컬에만 있으니 신고에 개인정보가 필요 없다).
25
27
  - **절대 사용자 데이터를 외부로 업로드하지 마라.** 모든 데이터는 `~/.careermate`(Windows: `%USERPROFILE%\.careermate`)에만 있고, 대시보드는 `127.0.0.1`에만 바인딩된다. CareerMate는 (선택적 버전 확인 외) 네트워크로 사용자 데이터를 보내지 않는다.
26
28
  - 기술 용어(MCP, `job_id` 등)는 사용자에게 노출하지 말고, 매 단계 무엇을 하는지 **사용자의 언어로** 쉽게 설명하라. 확실하지 않으면 지어내지 말고 사용자에게 확인하라.
27
29
 
@@ -32,6 +32,7 @@ EOP·이 문서를 맹신하지 말 것. **저장된 사용자 데이터나 공
32
32
  | "이 공고 적합도/나랑 맞아?/지원할 만해?" | [fit-analysis](eop/fit-analysis.md) | job-analysis, profile-extraction |
33
33
  | "이 공고 분석/뜯어줘", "무슨 회사·직무·레벨이야" | [job-analysis](eop/job-analysis.md) | — |
34
34
  | "이력서/경력 정리·입력", "기본정보 채워" | [profile-extraction](eop/profile-extraction.md) | — |
35
+ | "경력기술서/경력서술서/career description 써줘·정리해줘" | [career-description](eop/career-description.md) | profile-extraction, ats |
35
36
  | "자소서/자기소개서/지원동기/커버레터 써줘·고쳐줘" | [cover-letter](eop/cover-letter.md) | company-research, human-writing, fit-analysis |
36
37
  | "면접 준비/예상질문/모의면접" | [interview-prep](eop/interview-prep.md) | company-research, fit-analysis |
37
38
  | résumé·ATS·linkedin·portfolio·salary·offer·rejection·onboarding | `knowledge/` 플레이북 | — |
@@ -72,12 +73,12 @@ EOP·이 문서를 맹신하지 말 것. **저장된 사용자 데이터나 공
72
73
  - **충돌(데이터 vs 데이터, 공식자료 vs EOP):** §0 근거 우선순위 적용, 사실 보존, 사용자에게 알림.
73
74
  - **충돌(도메인 vs 도메인):** 여러 `knowledge/` 도메인이 같은 주제를 건드리면 정본(소유) 도메인을 따른다 — 연봉 숫자 전술=salary-negotiation(recruiter-screen은 게이트 맥락의 조건부 규칙만 상속), 오퍼 비교·가중치=offer-evaluation-decision, 30-60-90=onboarding-first-90-days, 탈락 진단=rejection-triage-iteration, 슬롭/키워드/정량 사전=공유 모듈. 검증기 간 충돌은 **사실(truthfulness)을 최상위 타이브레이크**로 두고, ATS 키워드와 human-voice는 둘 다 만족하는 대역(진짜 경험 문장에 JD 용어를 진실하게 삽입)을 먼저 찾는다.
74
75
 
75
- ## 5. EOP 목록
76
- [fit-analysis](eop/fit-analysis.md) · [job-analysis](eop/job-analysis.md) · [profile-extraction](eop/profile-extraction.md) · [cover-letter](eop/cover-letter.md) · [interview-prep](eop/interview-prep.md)
76
+ ## 5. EOP 목록 (6종)
77
+ [fit-analysis](eop/fit-analysis.md) · [job-analysis](eop/job-analysis.md) · [profile-extraction](eop/profile-extraction.md) · [cover-letter](eop/cover-letter.md) · [career-description](eop/career-description.md) · [interview-prep](eop/interview-prep.md)
77
78
 
78
79
  각 EOP 구조 = 전문가 프레임 · 입력 수집 · 분석 절차 · 한국/영문 특수 · 출력 계약 · 자가검증 루브릭 · 근거.
79
80
 
80
81
  ## 6. 디렉터리·기존 문서와의 관계 (무엇을 버리지 않는가)
81
82
  - **`knowledge/` (Phase A — 16 도메인 플레이북 + 6 verifiers)** = **정본 deep 지식**(대학 커리어센터·NCS·ATS 등 출처 충실). **버리지 않는다.** 각 EOP는 이 플레이북을 *실행 절차로 압축*하고 상단 "지식 출처"로 가리킨다 — EOP=얇은 실행층, knowledge=깊은 지식층. 함께 적용한다.
82
83
  - **`validation/`** = 새 리서치 4편(`research/`, knowledge **보강**: 검증 출처+한국 시장+리뷰어 루브릭) + 다중 LLM(cross-vendor) 교차검증 하니스(`runs/`, `PROTOCOL.md`, `MANDATE.md`). 런타임 소비 경로는 아님.
83
- - **`implementation/`** = CareerMate **개발자용** Phase B 내부 설계(`ARCHITECTURE.md`·`CONTRACT.md`·`EXPERTS.md`·`VERIFIERS.md`·`LOOP_ENGINE.md`·`EVALUATION.md`·`CONSENSUS_ENGINE.md`·`TODO.md`·`KNOWLEDGE.md`·`CONSUMPTION.md` + `phase-b2/`). MCP 도구·DB·det 엔진 등 빌드 스펙 — **외부 LLM 소비에는 불필요**(소비 진입점은 이 README + `eop/` + `knowledge/`).
84
+ - **`implementation/`** = CareerMate **개발자용** Phase B 내부 설계(`ARCHITECTURE.md`·`CONTRACT.md`·`EXPERTS.md`·`VERIFIERS.md`·`LOOP_ENGINE.md`·`EVALUATION.md`·`CONSENSUS_ENGINE.md`·`TODO.md`·`KNOWLEDGE.md`·`CONSUMPTION.md` + `phase-b2/`). MCP 도구·DB·det 엔진 등 빌드 스펙 — **외부 LLM 소비에는 불필요**(소비 진입점은 이 README + `eop/`(6 EOP) + `knowledge/`).
@@ -111,6 +111,8 @@
111
111
 
112
112
  저장은 `add_resume`으로 한다. 경력기술서 작성 산출물은 `kind=career_description`, `source=ai`를 사용하고, 제목은 사용 목적이 드러나게 쓴다. 대표 문서 지정은 사용자가 동의했을 때만 한다.
113
113
 
114
+ **자소서·이력서 등을 한 턴에 함께 요청받았더라도, 경력기술서 저장 content엔 경력기술서 본문만 넣는다.** 자기소개서는 별도로 `save_cover_letter_version`에 저장하고, 한 번의 `add_resume` 호출 본문에 자소서·이력서 같은 다른 문서를 이어붙이지 않는다(작성은 한 답변에 둘 다 보여줘도, *저장은 문서별 도구로 분리*한다).
115
+
114
116
  ## 7. 자가검증 루브릭
115
117
 
116
118
  저장 직전 아래 기준으로 스스로 점검하고 수정한다.
@@ -121,6 +123,7 @@
121
123
  - C3 자기소개서 문장을 그대로 옮겨 동기·감정 중심 산문이 됨.
122
124
  - C4 이력서/프로필/프로젝트와 기간·역할·성과가 충돌함.
123
125
  - C5 공동 작업을 개인 단독 성과로 과장함.
126
+ - C6 한 저장 본문에 경력기술서와 다른 문서(자기소개서 등)가 함께 들어 있음 — `add_resume` content엔 경력기술서만, 자소서는 `save_cover_letter_version`으로 분리 저장.
124
127
 
125
128
  **MAJOR:**
126
129
  - M1 직무 나열만 있고 행동·산출물·성과가 없음.
@@ -13,12 +13,15 @@
13
13
 
14
14
  ## 1. 입력 수집
15
15
  `get_application_context` → profile·대표 resume·경력/프로젝트·대상 job·기존 자소서·선호 문체. 사용자가 파일을 주면 `read_document`. 부족하면 사용자 확인.
16
+ **기존 자소서는 참고용이지 템플릿이 아니다(자기복제 주의).** 전문을 그대로 따라 쓰지 마라 — 특히 직전 자소서 1편을 변주만 하는 실패가 잦다. 합격 신호가 있는 과거 자소서(연결된 지원 status가 `document_passed` 이상 — `recent_applications`에서 `cover_letter_id`로 확인)가 있으면 그 *구조·앵글*을 참고하되 본문은 새로 쓴다. 차별화 절차는 §2-3.
16
17
  판정: **로케일**(ko 문항형 vs en 단일 레터), **문항 유형**(지원동기/성장과정/직무역량/입사후포부/약점·갈등), **글자수 상·하한과 count_mode**(공백포함/제외/byte — 실제 지원 필드로 검증; 미지정 시 공백포함 기본).
18
+ **문항·양식·글자수가 컨텍스트/공고에 없으면 작성 전 사용자에게 요청한다**(문항 텍스트·제출 양식·글자수 — 추측·임의 양식 금지). 끝내 없으면 일반 양식으로 진행함을 명시한다.
17
19
 
18
20
  ## 2. 분석·작성 절차
19
21
  1. **회사 리서치**(company-research): 미션·사업·제품·최근 활동·인재상 → 본인과 연결할 고유 포인트 확보.
20
22
  2. **문항 의도 해석:** 지원동기=여기 오게 한 *구체 경험*(표면 관심 X); 성장과정=동기의 *근원+서사 연결*(인생 연대기 X); 직무역량=역할의 *핵심 역량 1~2개 정확 식별+증명*; 입사후포부=*회사 사업방향에 묶인* 단기·중기·장기(전문가 포지셔닝) 로드맵(과대 금지). **연차 수치(1~3년 등)는 일반 로드맵 골격일 때만 — 본인 기여·성과에 '입사 후 1~2년은'처럼 저장·공고에 없는 기간을 단정하지 마라(출처 밖 기간 단정=C4).** <!-- applied: r7-cover-letter / R7 cross-vendor PASS cl-s1=94 PERFECT (opus생성·codex채점) / 2026-06-20 -->
21
23
  3. **본인 증거 매핑:** 저장 경력/프로젝트에서 문항·역량을 증명할 경험 **1~2개를 깊게** 선택(얕게 5~6개 나열 금지).
24
+ - **기존 자소서 차별화(있을 때 — 자기복제 방지):** 기존 자소서는 *참고*지 *템플릿*이 아니다. **(1) 사실은 일관되게** — 경험·수치·고유명사·사용자 보이스는 공고가 달라도 같아야 하며, 자기복제를 피하려고 억지로 비틀지 마라(사실 왜곡 = C4). **(2) 프레이밍·산문은 새로** — 직무 적합 앵글·why-this-company·도입 훅·문장 표현은 이 공고에 맞춰 새로 짠다. 같은 대표 경험을 또 쓸 거면 *이 공고의 요구역량·인재상에 맞는 다른 각*으로 재프레이밍하고, 가능하면 *다른 경험*으로 다양화한다. **직전 자소서를 회사명·일부 토큰만 바꿔 따라 쓰지 마라(가장 흔한 실패).**
22
25
  4. **초안 작성:** 항목별 **소제목** + **두괄식**(핵심 결론 먼저) → **STAR-lite**(S/T 짧게, A에 무게, R=전후 델타 정량) → **직무/인재상 명시 연결**. 회사 고유 근거 1~3문장("회사명 placeholder로 바꾸면 어색"이어야 함). 글자수 **상한의 ~70%+를 채우되**, 입사후포부는 앞 문단과 **다른 새 기여**로(같은 성과·표현 반복 금지).
23
26
  - **STAR 앵커 우선(사실 변경 금지·표현 강화 허용):** Action/Result는 저장된 '한 일'(도입·적용·운영·분석한 것)과 저장된 결과에 앵커한다. 저장된 행위·맥락은 풍부하게 서술해도 좋다(평가자가 중시하는 경험 서술형 환영). **금지는 발명뿐** — 저장 근거가 없는 진단 결과·방법/도구·원인 규명·실패율·문서화·후속 성과를 새로 지어내지 마라. 저장에 없는 과정을 꼭 암시해야 하면 "기록/근거로 범위를 좁혔다" 수준의 중립 한 구절까지만. <!-- applied: P4-star-anchor / closed 2026-06-20 — codex 규칙-텍스트 전용 표결 agree(4/4) + claude 3모델 만장일치 + R7 산출물 PASS 간접지지(cl-s1=94 opus생성·codex채점 / cl-s2 codex생성 90 PASS) -->
24
27
  - **프레이밍 경계(작성·평가 공통):** 일반 도메인 원리는 *후보 귀속 없이* 맥락으로 OK. 비즈니스 임팩트·문제 프레이밍은 *저장된 성과/지표가 암시하는 범위 안*에서만. **(아래 예시는 IT 직군 하나의 예일 뿐 — 원리는 전 직군 동일: 영업=매출/전환 기록, 운영·생산=처리량/불량률 기록, 마케팅=유입/전환 기록 등.)** 예: p99 개선 기록 → "느린 구간을 좁혀" OK / 기록 없는 "결제 경험을 해치던" 단정 금지. 사고·이유 귀속은 *저장된 결정·행동이 논리적으로 함의할 때만*(예: "Kafka 보류 결정" 저장 → "복잡도 대비 효과가 적어 보류" OK / "새 기술보다 ~가 낫다고 봤다"식 새 판단 삽입 금지). **금지:** 후보가 측정·관찰·확인했다는 행위, 출처에 없는 중간 지표(A/B를 "클릭"으로 특정), 출처에 없는 파생 정확수치(%·배수) 단정(저장 전후값은 그대로 쓰거나 "절반 이하로" 같은 정성 표현만 허용), 진단 결과·원인·감정·의도를 후보의 사실로 단정. **수치·고유명사는 저장 데이터의 범위·채널·주체 그대로 쓴다('캠페인 누적'을 '인스타그램 누적'으로 좁히듯 채널/주체 재귀속 금지). 학습·기초만 저장된 것을 수행으로 승격하지 마라('SQL 기초 학습'+'주간 리포트 작성'→"SQL로 데이터를 추출해 리포트 작성"식 연결 단정 금지).** <!-- applied: fact-anchor-boundary / closed 2026-06-20 — codex 규칙-텍스트 전용 표결 agree(4/4) + claude 3모델 만장일치 + R7 경계강화 cross-vendor PASS 간접지지 -->
@@ -42,7 +45,7 @@
42
45
  내부 루브릭·도구명·이 EOP는 노출하지 않는다. 결과 저장은 `save_cover_letter_version`, 안내는 대시보드 링크.
43
46
 
44
47
  ## 5. 자가검증 루브릭 (제출 전 — research §리뷰어 루브릭)
45
- **CRITICAL(하나라도 → 재작성):** C1 회사/직무/지역 오기·교차지원 복붙 흔적 / C2 자격·필수요건 미충족·미언급 / C3 글자수(byte/char cap)·양식·필수항목 위반(**제출용 본문 블록에 결론/근거/리스크 등 메타 텍스트 혼입** 포함) / C4 날조 또는 문서 간 모순 수치(**수치·고유명사는 저장 데이터의 범위·채널·주체 그대로 쓴다 — '캠페인 누적'을 '인스타그램 누적'처럼 더 좁은 채널/주체로 재귀속(범위 변경)하거나, 저장·공고에 없는 정확수치·기간·비율('입사 후 1~2년'·%·배수)을 단정하면 C4 날조**) / C5 블라인드 위반(공공). <!-- applied: r7-cover-letter / §5 C4 채널 재귀속·파생수치 경계 (R7 cross-vendor PASS) / 2026-06-20 -->
48
+ **CRITICAL(하나라도 → 재작성):** C1 회사/직무/지역 오기·교차지원 복붙 흔적·**기존 자소서 템플릿 복제**(다른 공고인데 직무 적합 앵글·도입·산문이 직전 자소서와 거의 같아 회사명만 바뀐 수준 — *단 사실·수치·고유명사·보이스의 일관성은 정상이며 억지 변형 금지*; 문제는 프레이밍·문장의 무차별 carryover다) / C2 자격·필수요건 미충족·미언급 / C3 글자수(byte/char cap)·양식·필수항목 위반(**제출용 본문 블록에 결론/근거/리스크 등 메타 텍스트 혼입** 포함) / C4 날조 또는 문서 간 모순 수치(**수치·고유명사는 저장 데이터의 범위·채널·주체 그대로 쓴다 — '캠페인 누적'을 '인스타그램 누적'처럼 더 좁은 채널/주체로 재귀속(범위 변경)하거나, 저장·공고에 없는 정확수치·기간·비율('입사 후 1~2년'·%·배수)을 단정하면 C4 날조**) / C5 블라인드 위반(공공). <!-- applied: r7-cover-letter / §5 C4 채널 재귀속·파생수치 경계 (R7 cross-vendor PASS) / 2026-06-20 -->
46
49
  **MAJOR:** M1 제너릭·비고유 지원동기(why-this-company 증명 없음) / M2 핵심 성과에 정량 결과 없음(노력어 의존) / M3 경험을 요구 역량/직무에 미연결(문항 의도 놓침) / M4 두괄식·소제목 약함 / M5 빈 형용사 슬롭·AI 균일 / M6 입사후포부 과대·사업방향 무관.
47
50
  **MINOR:** 오탈자·맞춤법 / 상한 대비 과소·과패딩 / 수동·약한 동사·"나" 관점(영문) / 항목 중복·불균등 가중 / (영문) 수신자 결함.
48
51
  **통과 = CRITICAL 0 + MAJOR 0 + 슬롭/제너릭 없음 + 모든 수치가 저장 데이터에 앵커.**
@@ -11,7 +11,7 @@
11
11
  - 전이가능 스킬은 **자기주장이 아니라 행동 증거**로만 인정.
12
12
  - 과적합(overqualified)은 자동 탈락이 아니라 **리스크+완화질문**으로 표시.
13
13
  - 전역 직감 점수 금지 — **요건별로 증거를 인용하고 강도 등급**을 매긴다(구조화 채점이 인상 채점을 이긴다).
14
- - 높은 키워드 커버리지를 높은 적합으로 등치하지 않는다(>75~80%는 스터핑 신호).
14
+ - 높은 키워드 커버리지를 높은 적합으로 등치하지 않는다(여기 커버리지=*이력서의 JD 키워드 밀도*, >75~80%는 스터핑 신호 — [job-analysis](job-analysis.md) §0의 요건 *충족 %*와는 다른 축이며 둘 다 합격 게이트가 아니다).
15
15
 
16
16
  ## 0a. 발명 차단 — 사실 span 앵커 (판정영향 주장만)
17
17
  <!-- applied: r12-fit-contract-invention / 2026-06-21 (consensus R3 전원 agree; 결함-발생률 게이트) -->
@@ -14,7 +14,7 @@
14
14
  - **확신도 분리:** 원문 인용=확정, 그 외는 '추정'이며 확신도를 낮춰 표현한다(예: "이메일 접수형"은 확정, "수시 채용"은 낮은 확신의 추론). **외부 분류 코드(NCS 번호 등)는 데이터로 확정될 때만** 쓰고 아니면 계열명만(코드 발명 금지). 산출물에 'EOP'·내부 기준명을 노출하지 않는다.
15
15
 
16
16
  ## 1. 입력 수집
17
- 공고 텍스트(`get_application_context`의 job, 또는 사용자가 준 경로를 `read_document`/본문 붙여넣기). 회사·플랫폼 맥락.
17
+ 공고 텍스트(`get_application_context`의 job, 또는 사용자가 준 경로를 `read_document`/본문 붙여넣기). **공고가 URL(링크)이면 CareerMate가 가져오지 않으니, 네 브라우징·웹 도구로 본문을 직접 읽어 분석하고 `save_job_posting`에 텍스트로 저장한다(웹 도구가 없으면 사용자에게 본문을 붙여넣어 달라고 요청).** 회사·플랫폼 맥락.
18
18
 
19
19
  ## 2. 분석 절차
20
20
  1. **세그먼트:** 직무내용 / 자격요건(필수) / 우대사항 / 처우·복지 / 회사소개.
@@ -40,6 +40,7 @@
40
40
  - '성실한·책임감·열정·소통을 잘하는 / 꼼꼼한·끈기있는·헌신적인·주도적인 / hard-working·team player·detail-oriented·motivated·proactive·dedicated·diligent·people person' 같은 **증거 없는 평가형 형용사**나 그 동의어로 자신을 묘사하지 마라.
41
41
  - '~라고 생각합니다', '많은 것을 배웠습니다', '~을 통해 성장했습니다', '귀사' 같은 증거 없는 자기서술 종결구문을 남발하지 마라.
42
42
  - 이력서 불릿을 문장으로 바꿔 그대로 반복하지 마라.
43
+ - **다른 공고에 낸 자소서를 회사명·일부 토큰만 바꿔 재사용하지 마라(자기복제).** 사실·수치·보이스는 공고 간 일관되게 두되(억지 변형 금지), 직무 적합 앵글·도입·산문은 공고마다 새로 짠다. 자기복제는 C1 위반이고, 한국은 표절검사(무하유·Copy Killer)·AI 탐지에서 자기표절로 걸린다.
43
44
  - 회사명을 바꿔도 말이 되는 제너릭 지원동기를 쓰지 마라.
44
45
  - 한 통/한 문항에 5~6개 경험을 얕게 나열하지 마라(1~2개 깊게).
45
46
  - '규모'(담당 규모·팀 인원·연도)를 '성과'로 위장하지 마라 — 본인 행동의 변화량이 결과다.
@@ -63,11 +63,11 @@
63
63
  | ID | 검사 항목 | 합격 기준 | 측정 방법(LLM 없이 셀 수 있게) |
64
64
  |----|-----------|-----------|--------------------------------|
65
65
  | R1 | 모든 요건에 [필수/우대] 라벨 + 복합 요건 분해 | 라벨 누락 0개; 쉼표·'및'·'and/or'로 묶인 줄이 개별 항목으로 분리됨 | 추출 항목 수를 세고 각 항목에 'must'/'nice' 토큰 존재 확인(누락==0). 원문에서 구분자 포함 줄 수 대비 분해 항목 수가 ≥ 구분자+1 인지 검사 |
66
- | R2 | 충족·부분충족 판정에 출처 ID + evidence 존재 | status가 met·partial인데 evidence[] 배열이 빈 항목 0개 | (AI 셀프체크 기준으로 유효; **서버 강제는 Phase B** — 현재 save_fit_analysis 스키마에 requirements[]/evidence 필드 없음) met·partial 항목 수 대비 `evidence.length>0` 항목 수 비율 == 100% |
67
- | R3 | quotedBullet/quotedMetric이 원문 substring (환각·수치변형 차단) | 인용 문자열이 해당 sourceId 입력 텍스트의 부분문자열이 아닌 항목 0개 | (AI 셀프체크 기준으로 유효; **서버 강제는 Phase B** — 입력 구조 부재, 현재 연결 AI가 substring 수동 대조) 각 evidence의 quotedBullet을 입력 이력서/경력 텍스트에 대해 indexOf/`includes` 문자열 검색 → 미발견 개수 == 0 |
66
+ | R2 | 충족·부분충족 판정에 출처 ID + evidence 존재 | status가 met·partial인데 evidence[] 배열이 빈 항목 0개 | (AI 셀프체크 — 서버 자동검사는 아직 없음) met·partial 항목 수 대비 `evidence.length>0` 항목 수 비율 == 100% |
67
+ | R3 | quotedBullet/quotedMetric이 원문 substring (환각·수치변형 차단) | 인용 문자열이 해당 sourceId 입력 텍스트의 부분문자열이 아닌 항목 0개 | (AI 셀프체크 — 서버 자동검사는 아직 없음) 각 evidence의 quotedBullet을 입력 이력서/경력 텍스트에 대해 indexOf/`includes` 문자열 검색 → 미발견 개수 == 0 |
68
68
  | R4 | gap 표기 + must-have 미충족마다 완화 전략 | 미충족 must 중 mitigation 문장이 없는 항목 0개 | 미충족 must 개수 == mitigation 문장 개수 비교 |
69
69
  | R5 | must-have 충족률 수치 제시 + partial 제외 규칙 | 'met must 수 / 전체 must 수' 분수·% 1개 이상; 분자에 partial 미포함 | 분수/% 정규식(예: `\d+/\d+`, `\d+%`)이 must 문맥과 함께 1회 이상; 분자 == status=='met' 개수와 일치 확인 |
70
- | R6 | critical·hardGate gap 시 '권장 불가' 병기 (게이트 단독 우선) | hardGate 또는 critical이 gap인데 verdict가 'recommend'인 사례 0개 | (AI 셀프체크 기준으로 유효; **서버 강제는 Phase B** — verdict/requirements 필드 부재, 현재 연결 AI가 수동으로 '권장 불가' 강제) `requirements`에 `hardGate&&status==gap` 또는 `critical&&status==gap`이 1개라도 있으면 `verdict=='recommend'` 금지 |
70
+ | R6 | critical·hardGate gap 시 '권장 불가' 병기 (게이트 단독 우선) | hardGate 또는 critical이 gap인데 verdict가 'recommend'인 사례 0개 | (AI 셀프체크 — 서버 자동검사는 아직 없음) `requirements`에 `hardGate&&status==gap` 또는 `critical&&status==gap`이 1개라도 있으면 `verdict=='recommend'` 금지 |
71
71
  | R7 | 키워드 표면 존재가 의미 매칭과 분리 보고 | JD 핵심 키워드별 'O/X' 표기 1개 이상, 의미 매칭 충족 판정과 별도 필드 | 출력에 `keywordSurfacePresent` 또는 'O/X·있음/없음' 항목 카운트 ≥ 1 |
72
72
  | R8 | partial 남용 방지 (사유 강제) | partial 항목 중 '왜 full이 아닌지' 사유 문장이 없는 항목 0개 | partial 항목 수 == partialReason 문장 수 비교 |
73
73
  | R9 | gap 과소평가(false-negative) 방지 | gap으로 분류한 must마다 인접 증거 탐색 시도 흔적 존재 | gap must 개수 대비 `transferAttempt` 표기(예: "인접 검토: …") 항목 수 == 100% |
@@ -2,6 +2,8 @@
2
2
 
3
3
  > 이 문서는 CareerMate가 **연결된 AI에게 제공(serve)** 하는 지식이다. CareerMate 안에는 LLM이 없다 — 분석·작성·검증은 연결된 AI가 수행하고, 이 플레이북·루브릭은 그 AI가 **세계 최고 수준**으로 일하도록 안내한다.
4
4
 
5
+ > **역할 경계:** 이 문서는 글을 **쓰는·생성하는(write)** 단계의 플레이북이다(`get_playbook`, 작성 직전). 다 쓴 초안을 **저장 직전 점검·채점**하는 일은 짝 검증기 [`verifiers/human-voice.md`](verifiers/human-voice.md)(`get_verifier`)가 맡는다 — 같은 주제라도 *쓰기 지침*과 *검증 루브릭*은 역할이 다르고, 두 곳의 강도 차이(예: 사실 보존이 여기선 LLM-플래그, 검증기 C8·C9에선 하드 게이트)는 **의도된 것**이다. 두 문서는 슬롭·과장·클리셰·번역투 사전을 **의도적으로 각자 온전히 복제**해 들고 있다 — 한쪽만 호출돼도 사전이 온전하도록 함이니, **단일 출처로 합치거나 다른 문서 포인터로 대체하지 마라(독립 serve가 깨진다).**
6
+
5
7
  ## 1. 무엇을 하는가
6
8
 
7
9
  커리어 문서(자기소개서·자기 PR·지원 메일·면접 답변)를 "AI가 쓴 티" 없이, 사용자 고유의 목소리로, 사실을 보존하며 쓰도록 연결된 AI를 안내한다. CareerMate의 **1차 사용자층은 한국어 자소서 작성자**이므로 이 플레이북은 한국어 슬롭(번역투·클리셰·기계적 병렬·수동태)을 1차 기준으로, 영어 마커는 영문 지원서일 때만 적용하는 **보조 기준**으로 둔다. 이 문서는 기존 `packages/prompts/src/humanize.ts`의 정성 가이드(한국어 8개 항목, `epoko77-ai/im-not-ai` 근거)를 **상위 단일 원본으로 흡수·확장**하고, 거기에 "LLM 없이 셀 수 있는" 결정론 루브릭과 채용 적합성 축을 더한 것이다. 핵심은 슬롭 제거가 아니라 **"거짓 없고 직무에 맞닿은, 사람 목소리의 글"**을 만드는 것이다 — 슬롭이 0이어도 동문서답·내용 공백이면 떨어진다.
@@ -25,7 +25,7 @@
25
25
  | C5 | 이미지·아이콘·스킬바 부재 | 그래픽 요소 0개 | 이미지 마크업(`![]`, `<img`)·이모지/심볼로 표기된 연락처 아이콘·"skill bar" 채움문자(예: ●●●○○, █) 출현 횟수 **= 0** |
26
26
  | C6 | 연락처가 본문 상단에 위치 | 이메일·전화가 헤더/푸터가 아닌 본문 첫 영역에 존재 | 본문 텍스트 첫 15% 구간(또는 첫 5줄) 안에서 이메일 정규식(`[\w.+-]+@[\w-]+\.[\w.]+`) 매치 **≥ 1** 그리고 전화 패턴 매치 **≥ 1** |
27
27
  | C7 | 일관된 날짜 형식 | 경력/학력 기간이 ATS가 인식하는 형식으로 통일 | 날짜 토큰을 추출해 허용 패턴(`MMM YYYY`, `Month YYYY`, `MM/YYYY`, `YYYY.MM`, `YYYY-MM`) 중 **단일 패턴 비율 ≥ 90%**. 혼용·"현재/Present" 외 자유서술 날짜 비율 < 10% |
28
- | C8 *(advisory)* | JD 하드스킬 키워드 커버리지 | JD 상위 키워드의 충분 비율을 본문이 포함 | 산출물 본문에 존재하는 키워드 개수 M / JD 추출 키워드 N → **M/N ≥ 0.70 = 권장 출발점(advisory·비차단; 0.70은 법칙이 아니라 출발점이며 AI가 JD 규모·역할 수에 맞게 조정)**. 바로 옆 75%+(Jobscan 권장)/80%(스위트스폿)는 **비구속 상류 참조치 — 제2 임계값 아님**(단일 합격선은 0.70 하나). 매칭은 raw substring이 아니라 **외부 AI가 원문 JD·이력서 텍스트를 정규화·동의어 묶음(JD 요구사항 자체는 불변)한 토큰 집합을 토큰 경계로** 센다(raw substring 금지; ats.md R4 동일 원본; `Java`⊂`JavaScript` 거짓양성 차단). JD 미제공 시 N/A 처리하고 사유 기록 |
28
+ | C8 *(advisory)* | JD 하드스킬 키워드 커버리지 | JD 상위 키워드의 충분 비율을 본문이 포함 | 산출물 본문에 존재하는 키워드 개수 M / JD 추출 키워드 N → **M/N ≥ 0.70 = 권장 출발점(advisory·비차단; 0.70은 법칙이 아니라 출발점이며 AI가 JD 규모·역할 수에 맞게 조정)**. 바로 옆 75%+(Jobscan 권장)/80%(스위트스폿)는 **비구속 상류 참조치 — 제2 임계값 아님**(단일 합격선은 0.70 하나). 매칭은 raw substring이 아니라 **외부 AI가 원문 JD·이력서 텍스트를 정규화·동의어 묶음(JD 요구사항 자체는 불변)한 토큰 집합을 토큰 경계로** 센다(raw substring 금지; ats.md R4 동일 원본; `Java`⊂`JavaScript` 거짓양성 차단). JD 미제공 시 N/A 처리하고 사유 기록. **축 구분(메타):** 이 C8 = JD 키워드 *표면 존재(presence)* M/N≥0.70 **advisory**. `responsiveness-on-target` C8 = JD *필수요건 응답(response)* 커버리지 ≥80% **critical** — **다른 축**이라 한 자소서가 한쪽 통과·다른 쪽 미달이어도 **충돌이 아니다**(키워드가 표면에 있어도 요건에 *응답*하지 않을 수 있고, 요건에 응답해도 ATS 표면 키워드가 모자랄 수 있다) |
29
29
  | C9 | 직무명(target job title) 일치 | JD의 핵심 직무명이 본문에 등장 | JD 헤드라인 직무명을 본문(이력서 요약/타이틀, LinkedIn 헤드라인)에서 정확/근접 일치로 탐색 → 매치 **≥ 1** |
30
30
  | C10 | 키워드 스터핑 방지(과최적화) | 특정 키워드 반복이 과하지 않음 | C8 키워드 각각의 본문 등장 횟수 중 **어떤 단일 키워드도 6회 초과 금지**, 그리고 키워드 토큰이 전체 토큰에서 차지하는 비율 **< 6%**. 위반 키워드 개수 = 0 목표 |
31
31
  | C11 | 약어·풀네임 병기 | 핵심 약어가 풀네임과 함께 1회 이상 병기 | JD/본문 공통 약어(예: ATS, SQL, PM) 중 본문에 약어만 단독 등장하고 풀네임 동반이 0인 항목 개수 → **0 목표**(파서/사람 양쪽 매칭 보장) |
@@ -2,6 +2,8 @@
2
2
 
3
3
  > 이 검증기는 연결된 AI가 커리어 산출물(이력서·자소서·면접 답변·LinkedIn 등)을 점검할 때 적용하는 **크리틱 루브릭**이다. CareerMate는 루브릭을 제공하고, 실제 검사는 연결된 AI가 수행한다(내부 LLM 없음).
4
4
 
5
+ > **역할 경계:** 이 문서는 다 쓴 초안을 **저장 직전 점검·채점하는(verify)** 크리틱 루브릭이다(`get_verifier`, 저장 직전). 글을 **쓰는·생성하는** 단계의 지침은 짝 플레이북 [`../human-writing.md`](../human-writing.md)(`get_playbook`)에 있다 — *검증 루브릭*과 *쓰기 지침*은 역할이 다르고, 두 곳의 강도 차이(예: 사실 보존이 여기 C8·C9에선 하드 게이트, 플레이북에선 LLM-플래그)는 **의도된 것**이다. 두 문서의 사전은 **의도적으로 각자 온전히 복제**돼 있다(독립 호출 안전) — **단일 출처화·포인터 대체 금지(한쪽만 불려오면 사전이 사라진다).**
6
+
5
7
  ## 1. 목적
6
8
 
7
9
  이 검증기는 커리어 산출물이 **"AI가 쓴 티"가 나지 않고 사람의 목소리로 읽히는지**를 보장한다. 채용 담당자는 천편일률적인 자동 생성 글을 즉시 알아채고 신뢰를 거둔다. 구체적으로 다음 네 가지를 본다.
@@ -34,7 +34,7 @@
34
34
  | C5 | 형식 토큰 충족 | 문항이 명시한 형식 요소가 모두 존재. 예: STAR 면접이면 Situation·Task·Action·Result 4요소, "구체적 사례" 요구면 사례 1건+ | det/D · 형식 체크리스트별 대응 토큰/구간 존재 카운트 → 누락 요소 수 = 0 |
35
35
  | C6 | 글자 수/분량 준수 | 자소서 문항의 **글자 수 상·하한**을 지킴(한국 공채 자사양식의 핵심 탈락 사유). 범위 내 | det/D · 응답 길이(공백 포함/제외 둘 다) vs 문항 제한. 초과·미달 글자 수 출력 |
36
36
  | C7 | 질문 직접 응답성 | 응답이 질문에 **직접** 답하고 우회·일반론으로 빠지지 않음(off-target/회피 0건) | ai/H · 연결 AI가 "이 응답이 이 질문에 답하는가"를 Y/N 판정. N이면 빗나간 지점 명시 |
37
- | C8 | JD 필수 요건 커버리지 | JD의 **필수(required) 요건** 항목 중 산출물(이력서+자소서)에서 다뤄진 비율 **≥ 80%**, 필수 요건 미커버 **0개**. (이 ≥80%는 응답성 커버리지 신호이며 적합(fit) 권장 게이트가 아니다 — 적합 판정은 `fit-matching`/`fit-analysis`가 하드요건(knockout)으로 내린다. C8 자체는 응답성 루프 내 critical 검사로, 미달은 적합·지원을 차단하는 게 아니라 §3 보강 재작성을 트리거한다.) | det/D · JD에서 필수/우대 요건 분리 추출 → 각 요건 키워드가 산출물에 존재하는지 매칭 → (커버 요건 ÷ 전체 필수). 미커버 필수 요건 나열 |
37
+ | C8 | JD 필수 요건 커버리지 | JD의 **필수(required) 요건** 항목 중 산출물(이력서+자소서)에서 다뤄진 비율 **≥ 80%**, 필수 요건 미커버 **0개**. (이 ≥80%는 응답성 커버리지 신호이며 적합(fit) 권장 게이트가 아니다 — 적합 판정은 `fit-matching`/`fit-analysis`가 하드요건(knockout)으로 내린다. C8 자체는 응답성 루프 내 critical 검사로, 미달은 적합·지원을 차단하는 게 아니라 §3 보강 재작성을 트리거한다.) | det/D · JD에서 필수/우대 요건 분리 추출 → 각 요건 키워드가 산출물에 존재하는지 매칭 → (커버 요건 ÷ 전체 필수). 미커버 필수 요건 나열. **축 구분(메타):** 이 C8 = JD *필수요건 응답(response)* 커버리지 ≥80% **critical**. `ats-compat` C8 = JD 키워드 *표면 존재(presence)* M/N≥0.70 **advisory** — **다른 축**이라 두 게이트의 숫자·방향이 달라도 **충돌이 아니다**(요건에 응답해도 ATS 표면 키워드가 모자랄 수 있고, 키워드가 표면에 있어도 요건에 응답 안 할 수 있다) |
38
38
  | C9 | JD 우대 요건 커버리지 | **우대(preferred)** 요건 커버 비율 보고(차단 아님, 경고). 권장 ≥ 50% | det/D · C8과 동일 로직을 우대 요건에 적용. 비율만 리포트, 미달은 warning |
39
39
  | C10 | 과잉/딴소리 비율 | 응답 중 질문과 무관한 문장(Q-keyword·directive 어느 것과도 무관) 비율 **< 20%** | det/D + ai/H · 각 문장을 질문 관련/무관으로 태깅(키워드 매칭=det, 환언 무관 판정=ai) → 무관 문장 비율 |
40
40
  | C11 | 질문 전제·제약 준수 | 질문이 건 제약(예: "**팀** 경험", "**실패** 사례", "최근 3년 내")을 응답이 위반하지 않음 | ai/H · 질문의 한정어(팀/개인, 성공/실패, 기간) 추출 → 응답 사례가 그 한정과 일치하는지 판정. 위반 사례 개수 = 0 |
package/dist/doctor.mjs CHANGED
@@ -75989,7 +75989,7 @@ var ApplicationContextSchema = external_exports.object({
75989
75989
  });
75990
75990
 
75991
75991
  // packages/shared/src/build-info.ts
75992
- var APP_VERSION = true ? "0.0.6" : "0.0.0-dev";
75992
+ var APP_VERSION = true ? "0.0.7" : "0.0.0-dev";
75993
75993
  var BUNDLED = true;
75994
75994
 
75995
75995
  // packages/shared/src/index.ts
@@ -77754,10 +77754,13 @@ var CAREER_ROUTES = {
77754
77754
  verifierSequence: ["truthfulness", "consistency", "responsiveness-on-target"],
77755
77755
  loop: "draft_verify_revise"
77756
77756
  },
77757
+ // R22(audit): responsiveness-on-target는 한국 문항형 자소서 전용 검증기(문항별 1:1 응답·
77758
+ // 글자수 상·하한 = "핵심 탈락 사유")인데 analyze_job에만 라우팅돼 있어 정작 자소서 자가검증
77759
+ // 루프에서 빠져 있었다. 동문서답·미응답 문항·글자수 초과를 잡도록 write_cover_letter에 추가.
77757
77760
  write_cover_letter: {
77758
77761
  eop: "cover-letter",
77759
77762
  expertSequence: ["cover-letter", "human-writing", "company-research"],
77760
- verifierSequence: ["human-voice", "truthfulness", "ats-compat", "consistency"],
77763
+ verifierSequence: ["human-voice", "truthfulness", "responsiveness-on-target", "ats-compat", "consistency"],
77761
77764
  loop: "draft_verify_revise"
77762
77765
  },
77763
77766
  write_career_description: {
@@ -77769,7 +77772,9 @@ var CAREER_ROUTES = {
77769
77772
  prepare_interview: {
77770
77773
  eop: "interview-prep",
77771
77774
  expertSequence: ["recruiter-screen", "interview-behavioral", "interview-technical", "company-research"],
77772
- verifierSequence: ["truthfulness", "consistency"],
77775
+ // R22(audit): recency-staleness 추가 — 면접의 "최근 소식"·회사 근황이 낡으면 거짓 신선도가
77776
+ // 되므로(company-research) 저장 전 신선도 점검을 푸시한다.
77777
+ verifierSequence: ["truthfulness", "consistency", "recency-staleness"],
77773
77778
  loop: "draft_verify_revise"
77774
77779
  },
77775
77780
  // R7 A2: manage_application_status had no route → rejection-triage / offer-evaluation /
@@ -77791,7 +77796,9 @@ var CAREER_ROUTES = {
77791
77796
  "onboarding-first-90-days",
77792
77797
  "networking-referrals"
77793
77798
  ],
77794
- verifierSequence: ["truthfulness", "consistency"]
77799
+ // R22(audit): recency-staleness 추가 — 연봉 벤치마크·오퍼 비교는 시점이 지나면 낡은 기준이
77800
+ // 되므로(salary-negotiation/offer-evaluation) 저장 전 신선도 점검을 푸시한다.
77801
+ verifierSequence: ["truthfulness", "consistency", "recency-staleness"]
77795
77802
  },
77796
77803
  // R7 후속: linkedin-profile·portfolio는 onboarding(이력서 임포트)과 단계 의미가
77797
77804
  // 달라(개인 브랜드 자산 *생성*) 전용 워크플로우로 분리(consensus 권고). networking은
@@ -79620,7 +79627,7 @@ ${r.warnings.join("\n")}`, {
79620
79627
  `\uBB38\uC11C \uC778\uC785 \uD3F4\uB354\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4:
79621
79628
  ${dir}
79622
79629
 
79623
- \uC5EC\uAE30\uC5D0 \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\xB7\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uB123\uACE0 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uC54C\uB824\uC8FC\uC138\uC694. (hwp/hwpx/docx\uB294 \uC790\uB3D9\uC73C\uB85C \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD558\uACE0, pdf\xB7\uC774\uBBF8\uC9C0\uB294 \uC81C\uAC00 \uC9C1\uC811 \uC77D\uC2B5\uB2C8\uB2E4.)
79630
+ \uC5EC\uAE30\uC5D0 \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\xB7\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uB123\uACE0 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uC54C\uB824\uC8FC\uC138\uC694. (hwp\xB7hwpx\xB7docx\xB7pdf\uB294 \uC790\uB3D9\uC73C\uB85C \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD558\uACE0, \uD14D\uC2A4\uD2B8 \uB808\uC774\uC5B4\uAC00 \uC5C6\uB294 \uC774\uBBF8\uC9C0\xB7\uC2A4\uCE94 PDF\uB9CC \uC81C\uAC00 \uC9C1\uC811 \uC77D\uC2B5\uB2C8\uB2E4.)
79624
79631
 
79625
79632
  \uD604\uC7AC \uD30C\uC77C:
79626
79633
  ${list}`,
@@ -79631,7 +79638,7 @@ ${list}`,
79631
79638
  {
79632
79639
  name: "read_inbox",
79633
79640
  title: "\uC778\uC785 \uD3F4\uB354 \uD30C\uC77C \uC77D\uAE30",
79634
- description: '\uC778\uC785 \uD3F4\uB354(~/.careermate/inbox)\uC5D0 \uC0AC\uC6A9\uC790\uAC00 \uB123\uC5B4 \uB454 \uBB38\uC11C\uB4E4\uC758 \uBCF8\uBB38\uC744 \uD55C \uBC88\uC5D0 \uC77D\uC5B4 \uB3CC\uB824\uC90D\uB2C8\uB2E4. open_inbox\uB85C \uD3F4\uB354\uB97C \uC548\uB0B4\uD55C \uB4A4 \uC0AC\uC6A9\uC790\uAC00 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uD558\uBA74 \uD638\uCD9C\uD558\uC138\uC694. docx/hwp/hwpx/\uD14D\uC2A4\uD2B8\uB294 \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD558\uACE0, pdf\xB7\uC774\uBBF8\uC9C0\uB294 \uCD94\uCD9C\uD558\uC9C0 \uC54A\uACE0 \uD30C\uC77C \uACBD\uB85C\uB97C \uB3CC\uB824\uC8FC\uB2C8 AI \uD074\uB77C\uC774\uC5B8\uD2B8\uC758 \uD30C\uC77C \uC77D\uAE30 \uAE30\uB2A5\uC73C\uB85C \uADF8 \uACBD\uB85C\uB97C \uC9C1\uC811 \uC5F4\uC5B4 \uC77D\uC73C\uC138\uC694. \uC77D\uC740 \uB0B4\uC6A9\uC744 \uAD6C\uC870\uD654\uD574 \uD504\uB85C\uD544\uC740 save_profile, \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\uB294 add_resume, \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 save_cover_letter_version\uC73C\uB85C \uC800\uC7A5\uD558\uC138\uC694(\uD30C\uC77C \uB0B4\uC6A9\uC744 \uC0C8 \uD30C\uC77C\uB85C \uB2E4\uC2DC \uC4F0\uC9C0 \uB9C8\uC138\uC694). filename\uC744 \uC8FC\uBA74 \uD574\uB2F9 \uD30C\uC77C\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.',
79641
+ description: '\uC778\uC785 \uD3F4\uB354(~/.careermate/inbox)\uC5D0 \uC0AC\uC6A9\uC790\uAC00 \uB123\uC5B4 \uB454 \uBB38\uC11C\uB4E4\uC758 \uBCF8\uBB38\uC744 \uD55C \uBC88\uC5D0 \uC77D\uC5B4 \uB3CC\uB824\uC90D\uB2C8\uB2E4. open_inbox\uB85C \uD3F4\uB354\uB97C \uC548\uB0B4\uD55C \uB4A4 \uC0AC\uC6A9\uC790\uAC00 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uD558\uBA74 \uD638\uCD9C\uD558\uC138\uC694. docx\xB7hwp\xB7hwpx\xB7pdf\xB7\uD14D\uC2A4\uD2B8(.txt/.md)\uC5D0\uC11C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD574 \uB3CC\uB824\uC90D\uB2C8\uB2E4(read_document\uC640 \uB3D9\uC77C\uD55C \uCD94\uCD9C \uC2A4\uD0DD). \uB2E8 \uD14D\uC2A4\uD2B8 \uB808\uC774\uC5B4\uAC00 \uC5C6\uB294 \uC774\uBBF8\uC9C0\xB7\uC2A4\uCE94 PDF\uB294 \uCD94\uCD9C\uB418\uC9C0 \uC54A\uC544(unsupported=true) \uD30C\uC77C \uACBD\uB85C\uB9CC \uB3CC\uB824\uC8FC\uB2C8, \uADF8 \uACBD\uB85C\uB294 AI \uD074\uB77C\uC774\uC5B8\uD2B8\uC758 \uD30C\uC77C \uC77D\uAE30 \uAE30\uB2A5\uC73C\uB85C \uC9C1\uC811 \uC5F4\uC5B4 \uC77D\uC73C\uC138\uC694. \uC77D\uC740 \uB0B4\uC6A9\uC744 \uAD6C\uC870\uD654\uD574 \uD504\uB85C\uD544\uC740 save_profile, \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\uB294 add_resume, \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 save_cover_letter_version\uC73C\uB85C \uC800\uC7A5\uD558\uC138\uC694(\uD30C\uC77C \uB0B4\uC6A9\uC744 \uC0C8 \uD30C\uC77C\uB85C \uB2E4\uC2DC \uC4F0\uC9C0 \uB9C8\uC138\uC694). filename\uC744 \uC8FC\uBA74 \uD574\uB2F9 \uD30C\uC77C\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.',
79635
79642
  inputSchema: {
79636
79643
  filename: external_exports.string().optional().describe("\uD2B9\uC815 \uD30C\uC77C\uB9CC \uC77D\uC744 \uB54C \uD30C\uC77C\uBA85(\uB300\uC18C\uBB38\uC790 \uBB34\uC2DC). \uC0DD\uB7B5\uD558\uBA74 \uD3F4\uB354 \uC548 \uBAA8\uB4E0 \uD30C\uC77C\uC744 \uC77D\uC2B5\uB2C8\uB2E4."),
79637
79644
  max_chars: external_exports.number().optional().describe("\uD30C\uC77C\uBCC4 \uBC18\uD658 \uD14D\uC2A4\uD2B8 \uCD5C\uB300 \uAE38\uC774(\uCD08\uACFC \uC2DC \uC798\uB77C\uB0C4). \uAE30\uBCF8 20000\uC790")
@@ -79740,7 +79747,7 @@ ${lines}`;
79740
79747
  {
79741
79748
  name: "add_resume",
79742
79749
  title: "\uC774\uB825\uC11C/\uACBD\uB825\uAE30\uC220\uC11C \uCD94\uAC00",
79743
- description: "\uC774\uB825\uC11C, \uACBD\uB825\uAE30\uC220\uC11C, \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uB4F1 \uBB38\uC11C\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. content\uC5D0\uB294 \uC0AC\uC6A9\uC790\uAC00 \uC5C5\uB85C\uB4DC/\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8 \uB610\uB294 AI\uAC00 \uC804\uBB38\uAC00 \uC808\uCC28\uB85C \uC815\uB9AC\uD55C Markdown\uC744 \uB123\uC73C\uC138\uC694. \uACBD\uB825\uAE30\uC220\uC11C\uB97C \uC791\uC131\uD574 \uC800\uC7A5\uD560 \uB54C\uB294 kind=career_description, source=ai\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4. kind \uAE30\uBCF8\uAC12\uC740 resume\uC785\uB2C8\uB2E4. is_primary=true\uB85C \uB300\uD45C \uBB38\uC11C\uB97C \uC9C0\uC815\uD558\uBA74 get_application_context\uC758 primary_resume\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.",
79750
+ description: "\uC774\uB825\uC11C, \uACBD\uB825\uAE30\uC220\uC11C, \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uB4F1 \uBB38\uC11C\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. content\uC5D0\uB294 \uC0AC\uC6A9\uC790\uAC00 \uC5C5\uB85C\uB4DC/\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8 \uB610\uB294 AI\uAC00 \uC804\uBB38\uAC00 \uC808\uCC28\uB85C \uC815\uB9AC\uD55C Markdown\uC744 \uB123\uC73C\uC138\uC694. \uACBD\uB825\uAE30\uC220\uC11C\uB97C \uC791\uC131\uD574 \uC800\uC7A5\uD560 \uB54C\uB294 kind=career_description, source=ai\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4. \u26A0\uFE0F content\uC5D0\uB294 \uC774 \uBB38\uC11C \uD55C \uC885\uB958\uC758 \uBCF8\uBB38\uB9CC \uB123\uACE0, \uC790\uAE30\uC18C\uAC1C\uC11C \uB4F1 \uB2E4\uB978 \uC885\uB958 \uBB38\uC11C\uB97C \uAC19\uC740 content\uC5D0 \uC774\uC5B4\uBD99\uC774\uC9C0 \uB9C8\uC138\uC694 \u2014 \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 save_cover_letter_version\uC73C\uB85C \uB530\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uD55C \uBC88\uC5D0 \uB458 \uB2E4 \uC694\uCCAD\uBC1B\uC558\uC5B4\uB3C4 \uD55C \uC800\uC7A5 \uD638\uCD9C\uC5D0 \uB450 \uBB38\uC11C\uB97C \uD569\uCE58\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4). kind \uAE30\uBCF8\uAC12\uC740 resume\uC785\uB2C8\uB2E4. is_primary=true\uB85C \uB300\uD45C \uBB38\uC11C\uB97C \uC9C0\uC815\uD558\uBA74 get_application_context\uC758 primary_resume\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.",
79744
79751
  // Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
79745
79752
  // the MCP ingestion path — the primary route for résumé text — inherits the
79746
79753
  // same caps as the web API. kind stays optional here (defaults to resume).
@@ -79916,7 +79923,7 @@ ${lines}`;
79916
79923
  {
79917
79924
  name: "save_cover_letter_version",
79918
79925
  title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804 \uC800\uC7A5",
79919
- description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4. \uC800\uC7A5 \uC9C1\uC804 \uC790\uB3D9\uC73C\uB85C "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58" \uC810\uAC80\uC744 \uB3CC\uB9BD\uB2C8\uB2E4 \u2014 \uBCF8\uBB38\uC758 \uC815\uB7C9 \uC218\uCE58\uAC00 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADFC\uAC70\uAC00 \uC5C6\uC73C\uBA74(\uD658\uAC01 \uC758\uC2EC) \uC800\uC7A5\uC774 \uB9C9\uD799\uB2C8\uB2E4. \uADF8\uB54C\uB294 \uC6D0\uBCF8\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force:true\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694.',
79926
+ description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. \u26A0\uFE0F content\uC5D0\uB294 \uC790\uAE30\uC18C\uAC1C\uC11C \uBCF8\uBB38\uB9CC \uB123\uACE0, \uACBD\uB825\uAE30\uC220\uC11C \uB4F1 \uB2E4\uB978 \uC885\uB958 \uBB38\uC11C\uB97C \uC774\uC5B4\uBD99\uC774\uC9C0 \uB9C8\uC138\uC694(\uACBD\uB825\uAE30\uC220\uC11C\uB294 add_resume(kind=career_description)\uB85C \uB530\uB85C \uC800\uC7A5). cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4. \uC800\uC7A5 \uC9C1\uC804 \uC790\uB3D9\uC73C\uB85C "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58" \uC810\uAC80\uC744 \uB3CC\uB9BD\uB2C8\uB2E4 \u2014 \uBCF8\uBB38\uC758 \uC815\uB7C9 \uC218\uCE58\uAC00 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADFC\uAC70\uAC00 \uC5C6\uC73C\uBA74(\uD658\uAC01 \uC758\uC2EC) \uC800\uC7A5\uC774 \uB9C9\uD799\uB2C8\uB2E4. \uADF8\uB54C\uB294 \uC6D0\uBCF8\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force:true\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694.',
79920
79927
  inputSchema: CoverLetterVersionInputSchema.shape,
79921
79928
  handler: (args) => {
79922
79929
  try {
@@ -80067,7 +80074,8 @@ ${lines}`;
80067
80074
  // 재직중=updated_at(정보 확인 시점) / 종료=end_date
80068
80075
  months,
80069
80076
  years: Math.round(months / 12 * 10) / 10,
80070
- data_age_months: e.is_current ? monthsBetween(e.updated_at, today) : null
80077
+ // 음수 방지(미래 updated_at·시계 오차) — 0 미만은 0으로 클램프
80078
+ data_age_months: e.is_current ? Math.max(0, monthsBetween(e.updated_at, today) ?? 0) : null
80071
80079
  };
80072
80080
  }).filter((t) => t != null);
80073
80081
  const enriched = {
@@ -80098,7 +80106,7 @@ ${lines}`;
80098
80106
  try {
80099
80107
  const fit = saveFitAnalysis(args);
80100
80108
  const suggested_next_action = {
80101
- prompt: "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694?",
80109
+ prompt: "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694? (\uD68C\uC0AC \uC9C0\uC815 \uBB38\uD56D\xB7\uC591\uC2DD\xB7\uAE00\uC790\uC218\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uC54C\uB824\uC8FC\uC2DC\uBA74 \uADF8\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD569\uB2C8\uB2E4)",
80102
80110
  options: [
80103
80111
  {
80104
80112
  label: "\uB124, \uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uC918",
@@ -80117,7 +80125,8 @@ ${lines}`;
80117
80125
  "",
80118
80126
  "\uC0AC\uC6A9\uC790\uC5D0\uAC8C \uB2E4\uC74C \uC120\uD0DD\uC9C0\uB97C \uBCF4\uC5EC\uC8FC\uC138\uC694:",
80119
80127
  "1. \uB124, \uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uC918",
80120
- "2. \uC544\uB2C8\uC694, \uBD84\uC11D\uB9CC \uC800\uC7A5\uD574\uC918"
80128
+ "2. \uC544\uB2C8\uC694, \uBD84\uC11D\uB9CC \uC800\uC7A5\uD574\uC918",
80129
+ "(1\uBC88\uC774\uBA74, \uD68C\uC0AC \uC9C0\uC815 \uBB38\uD56D\xB7\uC591\uC2DD\xB7\uAE00\uC790\uC218\uAC00 \uC788\uC73C\uBA74 \uC54C\uB824\uB2EC\uB77C\uACE0 \uC694\uCCAD\uD558\uC138\uC694 \u2014 \uD55C\uAD6D \uC790\uC18C\uC11C\uB294 \uBB38\uD56D\uD615\uC774 \uB9CE\uC2B5\uB2C8\uB2E4. \uC5C6\uC73C\uBA74 \uC77C\uBC18 \uC591\uC2DD\uC73C\uB85C \uC9C4\uD589\uD569\uB2C8\uB2E4.)"
80121
80130
  ].join("\n"),
80122
80131
  {
80123
80132
  ...fit,
package/dist/mcp.mjs CHANGED
@@ -93019,7 +93019,7 @@ var ApplicationContextSchema = external_exports.object({
93019
93019
  });
93020
93020
 
93021
93021
  // packages/shared/src/build-info.ts
93022
- var APP_VERSION = true ? "0.0.6" : "0.0.0-dev";
93022
+ var APP_VERSION = true ? "0.0.7" : "0.0.0-dev";
93023
93023
  var BUNDLED = true;
93024
93024
 
93025
93025
  // packages/shared/src/index.ts
@@ -94784,10 +94784,13 @@ var CAREER_ROUTES = {
94784
94784
  verifierSequence: ["truthfulness", "consistency", "responsiveness-on-target"],
94785
94785
  loop: "draft_verify_revise"
94786
94786
  },
94787
+ // R22(audit): responsiveness-on-target는 한국 문항형 자소서 전용 검증기(문항별 1:1 응답·
94788
+ // 글자수 상·하한 = "핵심 탈락 사유")인데 analyze_job에만 라우팅돼 있어 정작 자소서 자가검증
94789
+ // 루프에서 빠져 있었다. 동문서답·미응답 문항·글자수 초과를 잡도록 write_cover_letter에 추가.
94787
94790
  write_cover_letter: {
94788
94791
  eop: "cover-letter",
94789
94792
  expertSequence: ["cover-letter", "human-writing", "company-research"],
94790
- verifierSequence: ["human-voice", "truthfulness", "ats-compat", "consistency"],
94793
+ verifierSequence: ["human-voice", "truthfulness", "responsiveness-on-target", "ats-compat", "consistency"],
94791
94794
  loop: "draft_verify_revise"
94792
94795
  },
94793
94796
  write_career_description: {
@@ -94799,7 +94802,9 @@ var CAREER_ROUTES = {
94799
94802
  prepare_interview: {
94800
94803
  eop: "interview-prep",
94801
94804
  expertSequence: ["recruiter-screen", "interview-behavioral", "interview-technical", "company-research"],
94802
- verifierSequence: ["truthfulness", "consistency"],
94805
+ // R22(audit): recency-staleness 추가 — 면접의 "최근 소식"·회사 근황이 낡으면 거짓 신선도가
94806
+ // 되므로(company-research) 저장 전 신선도 점검을 푸시한다.
94807
+ verifierSequence: ["truthfulness", "consistency", "recency-staleness"],
94803
94808
  loop: "draft_verify_revise"
94804
94809
  },
94805
94810
  // R7 A2: manage_application_status had no route → rejection-triage / offer-evaluation /
@@ -94821,7 +94826,9 @@ var CAREER_ROUTES = {
94821
94826
  "onboarding-first-90-days",
94822
94827
  "networking-referrals"
94823
94828
  ],
94824
- verifierSequence: ["truthfulness", "consistency"]
94829
+ // R22(audit): recency-staleness 추가 — 연봉 벤치마크·오퍼 비교는 시점이 지나면 낡은 기준이
94830
+ // 되므로(salary-negotiation/offer-evaluation) 저장 전 신선도 점검을 푸시한다.
94831
+ verifierSequence: ["truthfulness", "consistency", "recency-staleness"]
94825
94832
  },
94826
94833
  // R7 후속: linkedin-profile·portfolio는 onboarding(이력서 임포트)과 단계 의미가
94827
94834
  // 달라(개인 브랜드 자산 *생성*) 전용 워크플로우로 분리(consensus 권고). networking은
@@ -96662,7 +96669,7 @@ ${r.warnings.join("\n")}`, {
96662
96669
  `\uBB38\uC11C \uC778\uC785 \uD3F4\uB354\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4:
96663
96670
  ${dir}
96664
96671
 
96665
- \uC5EC\uAE30\uC5D0 \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\xB7\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uB123\uACE0 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uC54C\uB824\uC8FC\uC138\uC694. (hwp/hwpx/docx\uB294 \uC790\uB3D9\uC73C\uB85C \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD558\uACE0, pdf\xB7\uC774\uBBF8\uC9C0\uB294 \uC81C\uAC00 \uC9C1\uC811 \uC77D\uC2B5\uB2C8\uB2E4.)
96672
+ \uC5EC\uAE30\uC5D0 \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\xB7\uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C \uD30C\uC77C\uC744 \uB123\uACE0 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uC54C\uB824\uC8FC\uC138\uC694. (hwp\xB7hwpx\xB7docx\xB7pdf\uB294 \uC790\uB3D9\uC73C\uB85C \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD558\uACE0, \uD14D\uC2A4\uD2B8 \uB808\uC774\uC5B4\uAC00 \uC5C6\uB294 \uC774\uBBF8\uC9C0\xB7\uC2A4\uCE94 PDF\uB9CC \uC81C\uAC00 \uC9C1\uC811 \uC77D\uC2B5\uB2C8\uB2E4.)
96666
96673
 
96667
96674
  \uD604\uC7AC \uD30C\uC77C:
96668
96675
  ${list}`,
@@ -96673,7 +96680,7 @@ ${list}`,
96673
96680
  {
96674
96681
  name: "read_inbox",
96675
96682
  title: "\uC778\uC785 \uD3F4\uB354 \uD30C\uC77C \uC77D\uAE30",
96676
- description: '\uC778\uC785 \uD3F4\uB354(~/.careermate/inbox)\uC5D0 \uC0AC\uC6A9\uC790\uAC00 \uB123\uC5B4 \uB454 \uBB38\uC11C\uB4E4\uC758 \uBCF8\uBB38\uC744 \uD55C \uBC88\uC5D0 \uC77D\uC5B4 \uB3CC\uB824\uC90D\uB2C8\uB2E4. open_inbox\uB85C \uD3F4\uB354\uB97C \uC548\uB0B4\uD55C \uB4A4 \uC0AC\uC6A9\uC790\uAC00 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uD558\uBA74 \uD638\uCD9C\uD558\uC138\uC694. docx/hwp/hwpx/\uD14D\uC2A4\uD2B8\uB294 \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD558\uACE0, pdf\xB7\uC774\uBBF8\uC9C0\uB294 \uCD94\uCD9C\uD558\uC9C0 \uC54A\uACE0 \uD30C\uC77C \uACBD\uB85C\uB97C \uB3CC\uB824\uC8FC\uB2C8 AI \uD074\uB77C\uC774\uC5B8\uD2B8\uC758 \uD30C\uC77C \uC77D\uAE30 \uAE30\uB2A5\uC73C\uB85C \uADF8 \uACBD\uB85C\uB97C \uC9C1\uC811 \uC5F4\uC5B4 \uC77D\uC73C\uC138\uC694. \uC77D\uC740 \uB0B4\uC6A9\uC744 \uAD6C\uC870\uD654\uD574 \uD504\uB85C\uD544\uC740 save_profile, \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\uB294 add_resume, \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 save_cover_letter_version\uC73C\uB85C \uC800\uC7A5\uD558\uC138\uC694(\uD30C\uC77C \uB0B4\uC6A9\uC744 \uC0C8 \uD30C\uC77C\uB85C \uB2E4\uC2DC \uC4F0\uC9C0 \uB9C8\uC138\uC694). filename\uC744 \uC8FC\uBA74 \uD574\uB2F9 \uD30C\uC77C\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.',
96683
+ description: '\uC778\uC785 \uD3F4\uB354(~/.careermate/inbox)\uC5D0 \uC0AC\uC6A9\uC790\uAC00 \uB123\uC5B4 \uB454 \uBB38\uC11C\uB4E4\uC758 \uBCF8\uBB38\uC744 \uD55C \uBC88\uC5D0 \uC77D\uC5B4 \uB3CC\uB824\uC90D\uB2C8\uB2E4. open_inbox\uB85C \uD3F4\uB354\uB97C \uC548\uB0B4\uD55C \uB4A4 \uC0AC\uC6A9\uC790\uAC00 "\uB2E4 \uB123\uC5C8\uC5B4"\uB77C\uACE0 \uD558\uBA74 \uD638\uCD9C\uD558\uC138\uC694. docx\xB7hwp\xB7hwpx\xB7pdf\xB7\uD14D\uC2A4\uD2B8(.txt/.md)\uC5D0\uC11C \uBCF8\uBB38 \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD574 \uB3CC\uB824\uC90D\uB2C8\uB2E4(read_document\uC640 \uB3D9\uC77C\uD55C \uCD94\uCD9C \uC2A4\uD0DD). \uB2E8 \uD14D\uC2A4\uD2B8 \uB808\uC774\uC5B4\uAC00 \uC5C6\uB294 \uC774\uBBF8\uC9C0\xB7\uC2A4\uCE94 PDF\uB294 \uCD94\uCD9C\uB418\uC9C0 \uC54A\uC544(unsupported=true) \uD30C\uC77C \uACBD\uB85C\uB9CC \uB3CC\uB824\uC8FC\uB2C8, \uADF8 \uACBD\uB85C\uB294 AI \uD074\uB77C\uC774\uC5B8\uD2B8\uC758 \uD30C\uC77C \uC77D\uAE30 \uAE30\uB2A5\uC73C\uB85C \uC9C1\uC811 \uC5F4\uC5B4 \uC77D\uC73C\uC138\uC694. \uC77D\uC740 \uB0B4\uC6A9\uC744 \uAD6C\uC870\uD654\uD574 \uD504\uB85C\uD544\uC740 save_profile, \uC774\uB825\uC11C\xB7\uACBD\uB825\uAE30\uC220\uC11C\xB7\uD3EC\uD2B8\uD3F4\uB9AC\uC624\uB294 add_resume, \uAE30\uC874 \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 save_cover_letter_version\uC73C\uB85C \uC800\uC7A5\uD558\uC138\uC694(\uD30C\uC77C \uB0B4\uC6A9\uC744 \uC0C8 \uD30C\uC77C\uB85C \uB2E4\uC2DC \uC4F0\uC9C0 \uB9C8\uC138\uC694). filename\uC744 \uC8FC\uBA74 \uD574\uB2F9 \uD30C\uC77C\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.',
96677
96684
  inputSchema: {
96678
96685
  filename: external_exports.string().optional().describe("\uD2B9\uC815 \uD30C\uC77C\uB9CC \uC77D\uC744 \uB54C \uD30C\uC77C\uBA85(\uB300\uC18C\uBB38\uC790 \uBB34\uC2DC). \uC0DD\uB7B5\uD558\uBA74 \uD3F4\uB354 \uC548 \uBAA8\uB4E0 \uD30C\uC77C\uC744 \uC77D\uC2B5\uB2C8\uB2E4."),
96679
96686
  max_chars: external_exports.number().optional().describe("\uD30C\uC77C\uBCC4 \uBC18\uD658 \uD14D\uC2A4\uD2B8 \uCD5C\uB300 \uAE38\uC774(\uCD08\uACFC \uC2DC \uC798\uB77C\uB0C4). \uAE30\uBCF8 20000\uC790")
@@ -96782,7 +96789,7 @@ ${lines}`;
96782
96789
  {
96783
96790
  name: "add_resume",
96784
96791
  title: "\uC774\uB825\uC11C/\uACBD\uB825\uAE30\uC220\uC11C \uCD94\uAC00",
96785
- description: "\uC774\uB825\uC11C, \uACBD\uB825\uAE30\uC220\uC11C, \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uB4F1 \uBB38\uC11C\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. content\uC5D0\uB294 \uC0AC\uC6A9\uC790\uAC00 \uC5C5\uB85C\uB4DC/\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8 \uB610\uB294 AI\uAC00 \uC804\uBB38\uAC00 \uC808\uCC28\uB85C \uC815\uB9AC\uD55C Markdown\uC744 \uB123\uC73C\uC138\uC694. \uACBD\uB825\uAE30\uC220\uC11C\uB97C \uC791\uC131\uD574 \uC800\uC7A5\uD560 \uB54C\uB294 kind=career_description, source=ai\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4. kind \uAE30\uBCF8\uAC12\uC740 resume\uC785\uB2C8\uB2E4. is_primary=true\uB85C \uB300\uD45C \uBB38\uC11C\uB97C \uC9C0\uC815\uD558\uBA74 get_application_context\uC758 primary_resume\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.",
96792
+ description: "\uC774\uB825\uC11C, \uACBD\uB825\uAE30\uC220\uC11C, \uD3EC\uD2B8\uD3F4\uB9AC\uC624 \uB4F1 \uBB38\uC11C\uB97C \uC800\uC7A5\uD569\uB2C8\uB2E4. content\uC5D0\uB294 \uC0AC\uC6A9\uC790\uAC00 \uC5C5\uB85C\uB4DC/\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8 \uB610\uB294 AI\uAC00 \uC804\uBB38\uAC00 \uC808\uCC28\uB85C \uC815\uB9AC\uD55C Markdown\uC744 \uB123\uC73C\uC138\uC694. \uACBD\uB825\uAE30\uC220\uC11C\uB97C \uC791\uC131\uD574 \uC800\uC7A5\uD560 \uB54C\uB294 kind=career_description, source=ai\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4. \u26A0\uFE0F content\uC5D0\uB294 \uC774 \uBB38\uC11C \uD55C \uC885\uB958\uC758 \uBCF8\uBB38\uB9CC \uB123\uACE0, \uC790\uAE30\uC18C\uAC1C\uC11C \uB4F1 \uB2E4\uB978 \uC885\uB958 \uBB38\uC11C\uB97C \uAC19\uC740 content\uC5D0 \uC774\uC5B4\uBD99\uC774\uC9C0 \uB9C8\uC138\uC694 \u2014 \uC790\uAE30\uC18C\uAC1C\uC11C\uB294 save_cover_letter_version\uC73C\uB85C \uB530\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uD55C \uBC88\uC5D0 \uB458 \uB2E4 \uC694\uCCAD\uBC1B\uC558\uC5B4\uB3C4 \uD55C \uC800\uC7A5 \uD638\uCD9C\uC5D0 \uB450 \uBB38\uC11C\uB97C \uD569\uCE58\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4). kind \uAE30\uBCF8\uAC12\uC740 resume\uC785\uB2C8\uB2E4. is_primary=true\uB85C \uB300\uD45C \uBB38\uC11C\uB97C \uC9C0\uC815\uD558\uBA74 get_application_context\uC758 primary_resume\uB85C \uB178\uCD9C\uB429\uB2C8\uB2E4.",
96786
96793
  // Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
96787
96794
  // the MCP ingestion path — the primary route for résumé text — inherits the
96788
96795
  // same caps as the web API. kind stays optional here (defaults to resume).
@@ -96958,7 +96965,7 @@ ${lines}`;
96958
96965
  {
96959
96966
  name: "save_cover_letter_version",
96960
96967
  title: "\uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804 \uC800\uC7A5",
96961
- description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4. \uC800\uC7A5 \uC9C1\uC804 \uC790\uB3D9\uC73C\uB85C "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58" \uC810\uAC80\uC744 \uB3CC\uB9BD\uB2C8\uB2E4 \u2014 \uBCF8\uBB38\uC758 \uC815\uB7C9 \uC218\uCE58\uAC00 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADFC\uAC70\uAC00 \uC5C6\uC73C\uBA74(\uD658\uAC01 \uC758\uC2EC) \uC800\uC7A5\uC774 \uB9C9\uD799\uB2C8\uB2E4. \uADF8\uB54C\uB294 \uC6D0\uBCF8\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force:true\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694.',
96968
+ description: '\uC790\uAE30\uC18C\uAC1C\uC11C\uC758 \uC0C8 \uBC84\uC804\uC744 \uC800\uC7A5\uD569\uB2C8\uB2E4. CareerMate \uC790\uC18C\uC11C \uC791\uC131 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 \uD575\uC2EC \uC800\uC7A5 \uB2E8\uACC4\uC785\uB2C8\uB2E4. \u26A0\uFE0F content\uC5D0\uB294 \uC790\uAE30\uC18C\uAC1C\uC11C \uBCF8\uBB38\uB9CC \uB123\uACE0, \uACBD\uB825\uAE30\uC220\uC11C \uB4F1 \uB2E4\uB978 \uC885\uB958 \uBB38\uC11C\uB97C \uC774\uC5B4\uBD99\uC774\uC9C0 \uB9C8\uC138\uC694(\uACBD\uB825\uAE30\uC220\uC11C\uB294 add_resume(kind=career_description)\uB85C \uB530\uB85C \uC800\uC7A5). cover_letter_id\uB97C \uC8FC\uBA74 \uAE30\uC874 \uC790\uC18C\uC11C\uC5D0 \uC0C8 \uBC84\uC804\uC744 \uCD94\uAC00\uD558\uACE0, \uC5C6\uC73C\uBA74 \uC0C8 \uC790\uC18C\uC11C\uB97C \uB9CC\uB4E4\uC5B4 v1\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(\uC774\uB54C title \uAD8C\uC7A5). note\uC5D0 \uC774 \uBC84\uC804\uC758 \uBCC0\uACBD \uC694\uC57D(\uC608: "\uC9C0\uC6D0\uB3D9\uAE30 \uBCF4\uAC15")\uC744 \uB0A8\uAE30\uBA74 \uC0AC\uC6A9\uC790\uAC00 \uB300\uC2DC\uBCF4\uB4DC\uC5D0\uC11C \uBC84\uC804 \uD788\uC2A4\uD1A0\uB9AC\uB97C \uC774\uD574\uD558\uAE30 \uC27D\uC2B5\uB2C8\uB2E4. job_id\uB85C \uACF5\uACE0\uC5D0 \uC5F0\uACB0\uD558\uBA74 \uC9C0\uC6D0 \uD56D\uBAA9\uACFC \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4. \uC800\uC7A5 \uC9C1\uC804 \uC790\uB3D9\uC73C\uB85C "\uADFC\uAC70 \uC5C6\uB294 \uC218\uCE58" \uC810\uAC80\uC744 \uB3CC\uB9BD\uB2C8\uB2E4 \u2014 \uBCF8\uBB38\uC758 \uC815\uB7C9 \uC218\uCE58\uAC00 \uC800\uC7A5\uB41C \uACBD\uB825\xB7\uC774\uB825\uC11C\xB7\uD504\uB85C\uC81D\uD2B8\uC5D0 \uADFC\uAC70\uAC00 \uC5C6\uC73C\uBA74(\uD658\uAC01 \uC758\uC2EC) \uC800\uC7A5\uC774 \uB9C9\uD799\uB2C8\uB2E4. \uADF8\uB54C\uB294 \uC6D0\uBCF8\uC758 \uC2E4\uC81C \uC218\uCE58\uB85C \uACE0\uCE58\uAC70\uB098, \uC758\uB3C4\uD55C \uAC12\uC774\uBA74 force:true\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uC138\uC694.',
96962
96969
  inputSchema: CoverLetterVersionInputSchema.shape,
96963
96970
  handler: (args) => {
96964
96971
  try {
@@ -97109,7 +97116,8 @@ ${lines}`;
97109
97116
  // 재직중=updated_at(정보 확인 시점) / 종료=end_date
97110
97117
  months,
97111
97118
  years: Math.round(months / 12 * 10) / 10,
97112
- data_age_months: e.is_current ? monthsBetween(e.updated_at, today) : null
97119
+ // 음수 방지(미래 updated_at·시계 오차) — 0 미만은 0으로 클램프
97120
+ data_age_months: e.is_current ? Math.max(0, monthsBetween(e.updated_at, today) ?? 0) : null
97113
97121
  };
97114
97122
  }).filter((t) => t != null);
97115
97123
  const enriched = {
@@ -97140,7 +97148,7 @@ ${lines}`;
97140
97148
  try {
97141
97149
  const fit = saveFitAnalysis(args);
97142
97150
  const suggested_next_action = {
97143
- prompt: "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694?",
97151
+ prompt: "\uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uB4DC\uB9B4\uAE4C\uC694? (\uD68C\uC0AC \uC9C0\uC815 \uBB38\uD56D\xB7\uC591\uC2DD\xB7\uAE00\uC790\uC218\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uC54C\uB824\uC8FC\uC2DC\uBA74 \uADF8\uC5D0 \uB9DE\uCDB0 \uC791\uC131\uD569\uB2C8\uB2E4)",
97144
97152
  options: [
97145
97153
  {
97146
97154
  label: "\uB124, \uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uC918",
@@ -97159,7 +97167,8 @@ ${lines}`;
97159
97167
  "",
97160
97168
  "\uC0AC\uC6A9\uC790\uC5D0\uAC8C \uB2E4\uC74C \uC120\uD0DD\uC9C0\uB97C \uBCF4\uC5EC\uC8FC\uC138\uC694:",
97161
97169
  "1. \uB124, \uC774 \uACF5\uACE0\uC5D0 \uB9DE\uCD98 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uC368\uC918",
97162
- "2. \uC544\uB2C8\uC694, \uBD84\uC11D\uB9CC \uC800\uC7A5\uD574\uC918"
97170
+ "2. \uC544\uB2C8\uC694, \uBD84\uC11D\uB9CC \uC800\uC7A5\uD574\uC918",
97171
+ "(1\uBC88\uC774\uBA74, \uD68C\uC0AC \uC9C0\uC815 \uBB38\uD56D\xB7\uC591\uC2DD\xB7\uAE00\uC790\uC218\uAC00 \uC788\uC73C\uBA74 \uC54C\uB824\uB2EC\uB77C\uACE0 \uC694\uCCAD\uD558\uC138\uC694 \u2014 \uD55C\uAD6D \uC790\uC18C\uC11C\uB294 \uBB38\uD56D\uD615\uC774 \uB9CE\uC2B5\uB2C8\uB2E4. \uC5C6\uC73C\uBA74 \uC77C\uBC18 \uC591\uC2DD\uC73C\uB85C \uC9C4\uD589\uD569\uB2C8\uB2E4.)"
97163
97172
  ].join("\n"),
97164
97173
  {
97165
97174
  ...fit,
@@ -97525,7 +97534,7 @@ async function main() {
97525
97534
  const server = new McpServer(
97526
97535
  { name: "careermate", version: APP_VERSION },
97527
97536
  {
97528
- instructions: "CareerMate\uB294 \uC0AC\uC6A9\uC790\uC758 \uB85C\uCEEC \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130\uBCA0\uC774\uC2A4\uC785\uB2C8\uB2E4. \uACF5\uACE0 \uBD84\uC11D, \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131, \uACBD\uB825\uAE30\uC220\uC11C \uC791\uC131 \uC804\uC5D0\uB294 \uD56D\uC0C1 \uC800\uC7A5\uB41C \uC0AC\uC6A9\uC790 \uC815\uBCF4\uB97C \uBA3C\uC800 \uAC00\uC838\uC624\uACE0(\uACF5\uACE0 \uC911\uC2EC \uC791\uC5C5\uC740 get_application_context \uC751\uB2F5\uC758 recommended_route\xB7next_tool\uAC00 \uB2E4\uC74C \uD638\uCD9C\uC744 \uC548\uB0B4\uD569\uB2C8\uB2E4), \uC774\uC5B4 get_workflow_guide\uB85C \uADF8 \uC791\uC5C5\uC758 \uC804\uBB38\uAC00 \uC2E4\uD589 \uC808\uCC28\uC640 \uAC80\uC99D \uC21C\uC11C\uB97C \uBC1B\uC544 \uC801\uC6A9\uD558\uC138\uC694. \uACB0\uACFC\uB294 save_fit_analysis / save_cover_letter_version / add_resume(kind=career_description) \uB4F1\uC73C\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uB418, \uC800\uC7A5 \uC9C1\uC804 \uC548\uB0B4\uB41C get_verifier \uB8E8\uBE0C\uB9AD\uC73C\uB85C \uB2F9\uC2E0\uC774 \uC2A4\uC2A4\uB85C \uC810\uAC80\uD558\uC138\uC694 \u2014 CareerMate\uB294 \uC140 \uC218 \uC788\uB294 \uD56D\uBAA9\uB9CC \uB3CC\uB824\uC8FC\uACE0 \uBD84\uC11D\xB7\uC791\uC131\xB7\uD310\uB2E8\uC740 \uB2F9\uC2E0(AI)\uC774 \uD569\uB2C8\uB2E4. \uBAA8\uB4E0 \uB370\uC774\uD130\uB294 \uC0AC\uC6A9\uC790 \uB85C\uCEEC\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uB2F5\uD560 \uB54C\uB294 \uD56D\uC0C1 \uC0AC\uC6A9\uC790\uAC00 \uC4F4 \uC5B8\uC5B4\uB85C \uB2F5\uD558\uC138\uC694 \u2014 \uC774 \uC9C0\uCE68\uACFC \uB3C4\uAD6C \uC751\uB2F5\uC774 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uC5EC \uC788\uC5B4\uB3C4 \uADF8\uB300\uB85C \uBCF5\uC0AC\uD558\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790 \uC5B8\uC5B4\uB85C \uC62E\uACA8 \uC804\uB2EC\uD558\uACE0, \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uBCF4\uC874\uD569\uB2C8\uB2E4."
97537
+ instructions: "CareerMate\uB294 \uC0AC\uC6A9\uC790\uC758 \uB85C\uCEEC \uCEE4\uB9AC\uC5B4 \uB370\uC774\uD130\uBCA0\uC774\uC2A4\uC785\uB2C8\uB2E4. \uACF5\uACE0 \uBD84\uC11D, \uC790\uAE30\uC18C\uAC1C\uC11C \uC791\uC131, \uACBD\uB825\uAE30\uC220\uC11C \uC791\uC131 \uC804\uC5D0\uB294 \uD56D\uC0C1 \uC800\uC7A5\uB41C \uC0AC\uC6A9\uC790 \uC815\uBCF4\uB97C \uBA3C\uC800 \uAC00\uC838\uC624\uACE0(\uACF5\uACE0 \uC911\uC2EC \uC791\uC5C5\uC740 get_application_context \uC751\uB2F5\uC758 recommended_route\xB7next_tool\uAC00 \uB2E4\uC74C \uD638\uCD9C\uC744 \uC548\uB0B4\uD569\uB2C8\uB2E4), \uC774\uC5B4 get_workflow_guide\uB85C \uADF8 \uC791\uC5C5\uC758 \uC804\uBB38\uAC00 \uC2E4\uD589 \uC808\uCC28\uC640 \uAC80\uC99D \uC21C\uC11C\uB97C \uBC1B\uC544 \uC801\uC6A9\uD558\uC138\uC694. \uACB0\uACFC\uB294 save_fit_analysis / save_cover_letter_version / add_resume(kind=career_description) \uB4F1\uC73C\uB85C \uB2E4\uC2DC \uC800\uC7A5\uD558\uB418(\uC5EC\uB7EC \uBB38\uC11C\uB97C \uD55C \uBC88\uC5D0 \uC694\uCCAD\uBC1B\uC544\uB3C4 \uAC01 \uBB38\uC11C\uB294 \uAC01\uC790\uC758 \uC800\uC7A5 \uB3C4\uAD6C\uB85C \uB530\uB85C \uC800\uC7A5\uD558\uACE0 \uD55C \uC800\uC7A5 \uBCF8\uBB38\uC5D0 \uB2E4\uB978 \uBB38\uC11C\uB97C \uC774\uC5B4\uBD99\uC774\uC9C0 \uB9C8\uC138\uC694 \u2014 \uACBD\uB825\uAE30\uC220\uC11C=add_resume, \uC790\uAE30\uC18C\uAC1C\uC11C=save_cover_letter_version), \uC800\uC7A5 \uC9C1\uC804 \uC548\uB0B4\uB41C get_verifier \uB8E8\uBE0C\uB9AD\uC73C\uB85C \uB2F9\uC2E0\uC774 \uC2A4\uC2A4\uB85C \uC810\uAC80\uD558\uC138\uC694 \u2014 CareerMate\uB294 \uC140 \uC218 \uC788\uB294 \uD56D\uBAA9\uB9CC \uB3CC\uB824\uC8FC\uACE0 \uBD84\uC11D\xB7\uC791\uC131\xB7\uD310\uB2E8\uC740 \uB2F9\uC2E0(AI)\uC774 \uD569\uB2C8\uB2E4. \uBAA8\uB4E0 \uB370\uC774\uD130\uB294 \uC0AC\uC6A9\uC790 \uB85C\uCEEC\uC5D0\uB9CC \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uB2F5\uD560 \uB54C\uB294 \uD56D\uC0C1 \uC0AC\uC6A9\uC790\uAC00 \uC4F4 \uC5B8\uC5B4\uB85C \uB2F5\uD558\uC138\uC694 \u2014 \uC774 \uC9C0\uCE68\uACFC \uB3C4\uAD6C \uC751\uB2F5\uC774 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uC5EC \uC788\uC5B4\uB3C4 \uADF8\uB300\uB85C \uBCF5\uC0AC\uD558\uC9C0 \uB9D0\uACE0 \uC0AC\uC6A9\uC790 \uC5B8\uC5B4\uB85C \uC62E\uACA8 \uC804\uB2EC\uD558\uACE0, \uC0AC\uC2E4\xB7\uC218\uCE58\xB7\uACE0\uC720\uBA85\uC0AC\uB294 \uBCF4\uC874\uD569\uB2C8\uB2E4."
97529
97538
  }
97530
97539
  );
97531
97540
  for (const tool of TOOLS) {
package/dist/web.mjs CHANGED
@@ -44314,7 +44314,7 @@ var ApplicationContextSchema = external_exports.object({
44314
44314
  });
44315
44315
 
44316
44316
  // packages/shared/src/build-info.ts
44317
- var APP_VERSION = true ? "0.0.6" : "0.0.0-dev";
44317
+ var APP_VERSION = true ? "0.0.7" : "0.0.0-dev";
44318
44318
  var BUNDLED = true;
44319
44319
 
44320
44320
  // packages/shared/src/index.ts
@@ -46100,6 +46100,7 @@ CareerMate\uB294 \uC0AC\uC6A9\uC790\uC758 \uCEF4\uD4E8\uD130\uC5D0\uC11C \uB3D9\
46100
46100
 
46101
46101
  2. \uACB0\uACFC\uB294 \uBC18\uB4DC\uC2DC \uB2E4\uC2DC \uC800\uC7A5\uD569\uB2C8\uB2E4.
46102
46102
  - \uBD84\uC11D\xB7\uC791\uC131\uD55C \uB0B4\uC6A9\uC740 \uBA38\uB9BF\uC18D\uC774\uB098 \uB300\uD654\uCC3D\uC5D0\uB9CC \uB0A8\uAE30\uC9C0 \uB9D0\uACE0 \uD574\uB2F9 \uC800\uC7A5 \uD234\uB85C CareerMate\uC5D0 \uAE30\uB85D\uD558\uC138\uC694: \uACF5\uACE0\uB294 \`save_job_posting\`, \uC801\uD569\uB3C4 \uBD84\uC11D\uC740 \`save_fit_analysis\`, \uC790\uAE30\uC18C\uAC1C\uC11C \uBC84\uC804\uC740 \`save_cover_letter_version\`, \uACBD\uB825\uAE30\uC220\uC11C\uB294 \`add_resume\`(kind=career_description), \uBA74\uC811 \uC900\uBE44\uB294 \`save_interview_prep\`, \uC0C1\uD0DC \uBCC0\uACBD\uC740 \`update_application_status\`.
46103
+ - \uC5EC\uB7EC \uBB38\uC11C\uB97C \uD55C \uBC88\uC5D0 \uC694\uCCAD\uBC1B\uC544\uB3C4 \uAC01 \uBB38\uC11C\uB294 \uAC01\uC790\uC758 \uC800\uC7A5 \uD234\uB85C \uB530\uB85C \uC800\uC7A5\uD558\uC138\uC694. \uACBD\uB825\uAE30\uC220\uC11C\uC640 \uC790\uAE30\uC18C\uAC1C\uC11C\uB97C \uD55C \uD134\uC5D0 \uD568\uAED8 \uB9CC\uB4E4\uC5C8\uB354\uB77C\uB3C4 \uD55C \uC800\uC7A5 \uD638\uCD9C\uC758 \uBCF8\uBB38\uC5D0 \uB2E4\uB978 \uBB38\uC11C\uB97C \uC774\uC5B4\uBD99\uC774\uC9C0 \uB9C8\uC138\uC694(\uACBD\uB825\uAE30\uC220\uC11C=\`add_resume\` kind=career_description, \uC790\uAE30\uC18C\uAC1C\uC11C=\`save_cover_letter_version\`\uB85C \uAC01\uAC01 \uBD84\uB9AC \uC800\uC7A5).
46103
46104
  - \uC800\uC7A5 \uD6C4\uC5D0\uB294 \uBB34\uC5C7\uC774 \uC800\uC7A5\uB418\uC5C8\uB294\uC9C0 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uAC04\uB2E8\uD788 \uC54C\uB824\uC8FC\uC138\uC694.
46104
46105
 
46105
46106
  3. \uB370\uC774\uD130\uB97C \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
package/docs/MCP_TOOLS.md CHANGED
@@ -43,7 +43,7 @@ CLI 클라이언트(Claude Code·Codex 등)는 파일 첨부가 안 되므로,
43
43
  | `read_inbox` | 인입 폴더에 넣어 둔 문서들의 본문을 한 번에 추출해 반환 | `filename?`(대소문자 무시, 생략 시 전체), `max_chars?`(파일별 기본 20000자) | `{ dir, files[] }`(각 `{ filename, path, format, unsupported, truncated, warnings[], text }`) | `open_inbox`로 안내한 뒤 사용자가 "다 넣었어"라고 할 때 | ✅ |
44
44
 
45
45
  - `open_inbox`는 폴더를 열기만 합니다. 사용자가 파일을 다 넣었다고 하면 `read_inbox`로 본문을 읽으세요.
46
- - `docx`/`hwp`/`hwpx`/텍스트는 텍스트를 추출하고, **`pdf`·이미지는 추출하지 않고 파일 경로만** 돌려주니(`unsupported`) AI 클라이언트의 파일 읽기 기능으로 그 경로를 직접 열어 읽으세요.
46
+ - `pdf`·`docx`·`hwp`/`hwpx`·텍스트는 본문 텍스트를 추출하고, **텍스트 레이어가 없는 이미지·스캔본 PDF만** 추출되지 않아 파일 경로만(`unsupported`) 돌려주니 AI 클라이언트의 파일 읽기 기능으로 그 경로를 직접 열어 읽으세요.
47
47
  - 읽은 내용은 그대로 새 파일로 다시 쓰지 말고, 구조화해 프로필은 `save_profile`, 이력서·경력기술서·포트폴리오는 `add_resume`, 기존 자기소개서는 `save_cover_letter_version`으로 저장하세요.
48
48
  - 응답 폭발을 막기 위해 파일별 본문은 `max_chars`(기본 20000자)로 잘라서 돌려주며, 잘린 경우 `truncated: true`로 표시합니다.
49
49
 
@@ -121,7 +121,7 @@ AI 클라이언트가 연결되면 이 도구를 가장 먼저 호출해 상태
121
121
  | 도구 | 목적 | 주요 입력 | 반환 | 언제 호출 | 읽기전용 |
122
122
  | --- | --- | --- | --- | --- | --- |
123
123
  | `get_cover_letters` | 저장된 자기소개서 조회(공고별 필터 가능). 각 항목은 현재 버전 내용·버전 수 포함 | `job_id?` | `CoverLetterRecord[]` | 자소서 목록/내용을 확인할 때 | ✅ |
124
- | `save_cover_letter_version` | 자기소개서의 새 버전 저장 — 자소서 작성 워크플로우의 핵심 저장 단계 | `content`(필수), `cover_letter_id?`, `title?`, `job_id?`, `note?`, `source?`, `set_current?` | `{ coverLetter, version }` | 새 공고용 자소서를 생성·갱신해 버전으로 남길 때 | ✏️ |
124
+ | `save_cover_letter_version` | 자기소개서의 새 버전 저장 — 자소서 작성 워크플로우의 핵심 저장 단계 | `content`(필수), `cover_letter_id?`, `title?`, `job_id?`, `note?`, `source?`, `set_current?`, `force?`, `strict?` | `{ coverLetter, version }` | 새 공고용 자소서를 생성·갱신해 버전으로 남길 때 | ✏️ |
125
125
  | `delete_cover_letter` | 자기소개서 삭제(모든 버전 포함) | `cover_letter_id`, `confirm:"DELETE"` | 삭제 결과 | 사용자가 목록에서 삭제 대상을 확인한 뒤 | ✏️ |
126
126
 
127
127
  `save_cover_letter_version` 동작(`CoverLetterVersionInputSchema`):
@@ -242,7 +242,7 @@ AI 클라이언트가 연결되면 이 도구를 가장 먼저 호출해 상태
242
242
  | --- | --- | --- | --- | --- | --- |
243
243
  | `get_workflow_guide` | 표준 워크플로우의 단계별 안내 + 그 작업의 **전문가 실행 절차(EOP)**·적용할 플레이북·저장 전 검증기 순서 반환 | `workflow_id?` | 단일 워크플로우 Markdown(EOP·플레이북/검증기 순서 포함) 또는 전체 목록(`{ id, title, trigger }[]`) | 커리어 작업을 시작하기 직전 | ✅ |
244
244
 
245
- `workflow_id` 값: `onboarding`, `analyze_job`, `write_cover_letter`, `write_career_description`, `manage_application_status`, `prepare_interview`. 생략하면 전체 목록을 돌려줍니다. `analyze_job`/`write_cover_letter`/`write_career_description`/`prepare_interview`/`onboarding`은 해당 작업의 EOP 본문과 `get_playbook`·`get_verifier` 호출 순서(`career_route`)를 함께 돌려줍니다.
245
+ `workflow_id` 값: `onboarding`, `analyze_job`, `write_cover_letter`, `write_career_description`, `manage_application_status`, `prepare_interview`, `build_personal_brand`. 생략하면 전체 목록을 돌려줍니다. `analyze_job`/`write_cover_letter`/`write_career_description`/`prepare_interview`/`onboarding`은 해당 작업의 EOP 본문과 `get_playbook`·`get_verifier` 호출 순서(`career_route`)를 함께 돌려줍니다.
246
246
 
247
247
  ---
248
248
 
@@ -329,6 +329,8 @@ CareerMate를 최신 버전으로 유지하기 위한 유지보수 도구입니
329
329
  | `recommended_route` | 맥락 기반 추천 워크플로우 id(`analyze_job`/`write_cover_letter`/`prepare_interview`/`onboarding`) 또는 `null`. 공고 중심 맥락이 아닌 `write_career_description`은 사용자 요청에 따라 직접 선택한다. `get_workflow_guide`의 `workflow_id`로 그대로 사용 |
330
330
  | `verifier_sequence` | 추천 경로에서 저장 직전 적용할 검증기 id 배열(Career-OS) |
331
331
  | `next_tool` | 다음에 부를 도구(보통 `get_workflow_guide`) 또는 `null` |
332
+ | `today` | 서버의 오늘 날짜(`YYYY-MM-DD`). LLM이 현재일을 신뢰성 있게 모르므로 주입(연차·신선도 판단 기준; MCP 응답 한정) |
333
+ | `tenure` | 재직기간을 서버가 결정적으로 계산한 배열 — 각 `{ company, is_current, start_date, as_of, months, years, data_age_months }`. **재직중=등록일 `updated_at` 기준 연차, 종료=`end_date` 기준.** 연차는 직접 산술하지 말고 이 값을 사용(`data_age_months` 크면 현재 재직 확인 필요). MCP 응답 한정 |
332
334
 
333
335
  **사용 흐름**
334
336
 
@@ -121,7 +121,7 @@
121
121
 
122
122
  ## 보조 도구
123
123
 
124
- - `get_workflow_guide` — 표준 워크플로우(온보딩, 공고 분석, 자소서 작성, 상태 관리, 면접 준비)의 단계별 안내. 어떤 순서로 어떤 도구를 호출할지 확인할 때 사용. `workflow_id` 생략 시 전체 목록.
124
+ - `get_workflow_guide` — 표준 워크플로우(온보딩, 공고 분석, 자소서 작성, 경력기술서 작성, 상태 관리, 면접 준비, 개인 브랜드)의 단계별 안내. 어떤 순서로 어떤 도구를 호출할지 확인할 때 사용. `workflow_id` 생략 시 전체 목록.
125
125
  - `list_recent_activity` — 최근 활동(공고 저장, 적합도 분석, 자소서 버전, 상태 변경, 면접 준비 등). "최근에 뭐 했지?" 또는 작업을 이어갈 맥락이 필요할 때.
126
126
  - `list_jobs` / `get_job_posting` — 저장된 공고 목록 / 단건 상세(적합도·상태·연결 자소서·면접 준비 포함).
127
127
  - `get_profile` / `get_resumes` / `get_cover_letters` — 저장된 데이터 조회.
@@ -32,6 +32,7 @@ EOP·이 문서를 맹신하지 말 것. **저장된 사용자 데이터나 공
32
32
  | "이 공고 적합도/나랑 맞아?/지원할 만해?" | [fit-analysis](eop/fit-analysis.md) | job-analysis, profile-extraction |
33
33
  | "이 공고 분석/뜯어줘", "무슨 회사·직무·레벨이야" | [job-analysis](eop/job-analysis.md) | — |
34
34
  | "이력서/경력 정리·입력", "기본정보 채워" | [profile-extraction](eop/profile-extraction.md) | — |
35
+ | "경력기술서/경력서술서/career description 써줘·정리해줘" | [career-description](eop/career-description.md) | profile-extraction, ats |
35
36
  | "자소서/자기소개서/지원동기/커버레터 써줘·고쳐줘" | [cover-letter](eop/cover-letter.md) | company-research, human-writing, fit-analysis |
36
37
  | "면접 준비/예상질문/모의면접" | [interview-prep](eop/interview-prep.md) | company-research, fit-analysis |
37
38
  | résumé·ATS·linkedin·portfolio·salary·offer·rejection·onboarding | `knowledge/` 플레이북 | — |
@@ -72,12 +73,12 @@ EOP·이 문서를 맹신하지 말 것. **저장된 사용자 데이터나 공
72
73
  - **충돌(데이터 vs 데이터, 공식자료 vs EOP):** §0 근거 우선순위 적용, 사실 보존, 사용자에게 알림.
73
74
  - **충돌(도메인 vs 도메인):** 여러 `knowledge/` 도메인이 같은 주제를 건드리면 정본(소유) 도메인을 따른다 — 연봉 숫자 전술=salary-negotiation(recruiter-screen은 게이트 맥락의 조건부 규칙만 상속), 오퍼 비교·가중치=offer-evaluation-decision, 30-60-90=onboarding-first-90-days, 탈락 진단=rejection-triage-iteration, 슬롭/키워드/정량 사전=공유 모듈. 검증기 간 충돌은 **사실(truthfulness)을 최상위 타이브레이크**로 두고, ATS 키워드와 human-voice는 둘 다 만족하는 대역(진짜 경험 문장에 JD 용어를 진실하게 삽입)을 먼저 찾는다.
74
75
 
75
- ## 5. EOP 목록
76
- [fit-analysis](eop/fit-analysis.md) · [job-analysis](eop/job-analysis.md) · [profile-extraction](eop/profile-extraction.md) · [cover-letter](eop/cover-letter.md) · [interview-prep](eop/interview-prep.md)
76
+ ## 5. EOP 목록 (6종)
77
+ [fit-analysis](eop/fit-analysis.md) · [job-analysis](eop/job-analysis.md) · [profile-extraction](eop/profile-extraction.md) · [cover-letter](eop/cover-letter.md) · [career-description](eop/career-description.md) · [interview-prep](eop/interview-prep.md)
77
78
 
78
79
  각 EOP 구조 = 전문가 프레임 · 입력 수집 · 분석 절차 · 한국/영문 특수 · 출력 계약 · 자가검증 루브릭 · 근거.
79
80
 
80
81
  ## 6. 디렉터리·기존 문서와의 관계 (무엇을 버리지 않는가)
81
82
  - **`knowledge/` (Phase A — 16 도메인 플레이북 + 6 verifiers)** = **정본 deep 지식**(대학 커리어센터·NCS·ATS 등 출처 충실). **버리지 않는다.** 각 EOP는 이 플레이북을 *실행 절차로 압축*하고 상단 "지식 출처"로 가리킨다 — EOP=얇은 실행층, knowledge=깊은 지식층. 함께 적용한다.
82
83
  - **`validation/`** = 새 리서치 4편(`research/`, knowledge **보강**: 검증 출처+한국 시장+리뷰어 루브릭) + 다중 LLM(cross-vendor) 교차검증 하니스(`runs/`, `PROTOCOL.md`, `MANDATE.md`). 런타임 소비 경로는 아님.
83
- - **`implementation/`** = CareerMate **개발자용** Phase B 내부 설계(`ARCHITECTURE.md`·`CONTRACT.md`·`EXPERTS.md`·`VERIFIERS.md`·`LOOP_ENGINE.md`·`EVALUATION.md`·`CONSENSUS_ENGINE.md`·`TODO.md`·`KNOWLEDGE.md`·`CONSUMPTION.md` + `phase-b2/`). MCP 도구·DB·det 엔진 등 빌드 스펙 — **외부 LLM 소비에는 불필요**(소비 진입점은 이 README + `eop/` + `knowledge/`).
84
+ - **`implementation/`** = CareerMate **개발자용** Phase B 내부 설계(`ARCHITECTURE.md`·`CONTRACT.md`·`EXPERTS.md`·`VERIFIERS.md`·`LOOP_ENGINE.md`·`EVALUATION.md`·`CONSENSUS_ENGINE.md`·`TODO.md`·`KNOWLEDGE.md`·`CONSUMPTION.md` + `phase-b2/`). MCP 도구·DB·det 엔진 등 빌드 스펙 — **외부 LLM 소비에는 불필요**(소비 진입점은 이 README + `eop/`(6 EOP) + `knowledge/`).
@@ -111,6 +111,8 @@
111
111
 
112
112
  저장은 `add_resume`으로 한다. 경력기술서 작성 산출물은 `kind=career_description`, `source=ai`를 사용하고, 제목은 사용 목적이 드러나게 쓴다. 대표 문서 지정은 사용자가 동의했을 때만 한다.
113
113
 
114
+ **자소서·이력서 등을 한 턴에 함께 요청받았더라도, 경력기술서 저장 content엔 경력기술서 본문만 넣는다.** 자기소개서는 별도로 `save_cover_letter_version`에 저장하고, 한 번의 `add_resume` 호출 본문에 자소서·이력서 같은 다른 문서를 이어붙이지 않는다(작성은 한 답변에 둘 다 보여줘도, *저장은 문서별 도구로 분리*한다).
115
+
114
116
  ## 7. 자가검증 루브릭
115
117
 
116
118
  저장 직전 아래 기준으로 스스로 점검하고 수정한다.
@@ -121,6 +123,7 @@
121
123
  - C3 자기소개서 문장을 그대로 옮겨 동기·감정 중심 산문이 됨.
122
124
  - C4 이력서/프로필/프로젝트와 기간·역할·성과가 충돌함.
123
125
  - C5 공동 작업을 개인 단독 성과로 과장함.
126
+ - C6 한 저장 본문에 경력기술서와 다른 문서(자기소개서 등)가 함께 들어 있음 — `add_resume` content엔 경력기술서만, 자소서는 `save_cover_letter_version`으로 분리 저장.
124
127
 
125
128
  **MAJOR:**
126
129
  - M1 직무 나열만 있고 행동·산출물·성과가 없음.
@@ -13,12 +13,15 @@
13
13
 
14
14
  ## 1. 입력 수집
15
15
  `get_application_context` → profile·대표 resume·경력/프로젝트·대상 job·기존 자소서·선호 문체. 사용자가 파일을 주면 `read_document`. 부족하면 사용자 확인.
16
+ **기존 자소서는 참고용이지 템플릿이 아니다(자기복제 주의).** 전문을 그대로 따라 쓰지 마라 — 특히 직전 자소서 1편을 변주만 하는 실패가 잦다. 합격 신호가 있는 과거 자소서(연결된 지원 status가 `document_passed` 이상 — `recent_applications`에서 `cover_letter_id`로 확인)가 있으면 그 *구조·앵글*을 참고하되 본문은 새로 쓴다. 차별화 절차는 §2-3.
16
17
  판정: **로케일**(ko 문항형 vs en 단일 레터), **문항 유형**(지원동기/성장과정/직무역량/입사후포부/약점·갈등), **글자수 상·하한과 count_mode**(공백포함/제외/byte — 실제 지원 필드로 검증; 미지정 시 공백포함 기본).
18
+ **문항·양식·글자수가 컨텍스트/공고에 없으면 작성 전 사용자에게 요청한다**(문항 텍스트·제출 양식·글자수 — 추측·임의 양식 금지). 끝내 없으면 일반 양식으로 진행함을 명시한다.
17
19
 
18
20
  ## 2. 분석·작성 절차
19
21
  1. **회사 리서치**(company-research): 미션·사업·제품·최근 활동·인재상 → 본인과 연결할 고유 포인트 확보.
20
22
  2. **문항 의도 해석:** 지원동기=여기 오게 한 *구체 경험*(표면 관심 X); 성장과정=동기의 *근원+서사 연결*(인생 연대기 X); 직무역량=역할의 *핵심 역량 1~2개 정확 식별+증명*; 입사후포부=*회사 사업방향에 묶인* 단기·중기·장기(전문가 포지셔닝) 로드맵(과대 금지). **연차 수치(1~3년 등)는 일반 로드맵 골격일 때만 — 본인 기여·성과에 '입사 후 1~2년은'처럼 저장·공고에 없는 기간을 단정하지 마라(출처 밖 기간 단정=C4).** <!-- applied: r7-cover-letter / R7 cross-vendor PASS cl-s1=94 PERFECT (opus생성·codex채점) / 2026-06-20 -->
21
23
  3. **본인 증거 매핑:** 저장 경력/프로젝트에서 문항·역량을 증명할 경험 **1~2개를 깊게** 선택(얕게 5~6개 나열 금지).
24
+ - **기존 자소서 차별화(있을 때 — 자기복제 방지):** 기존 자소서는 *참고*지 *템플릿*이 아니다. **(1) 사실은 일관되게** — 경험·수치·고유명사·사용자 보이스는 공고가 달라도 같아야 하며, 자기복제를 피하려고 억지로 비틀지 마라(사실 왜곡 = C4). **(2) 프레이밍·산문은 새로** — 직무 적합 앵글·why-this-company·도입 훅·문장 표현은 이 공고에 맞춰 새로 짠다. 같은 대표 경험을 또 쓸 거면 *이 공고의 요구역량·인재상에 맞는 다른 각*으로 재프레이밍하고, 가능하면 *다른 경험*으로 다양화한다. **직전 자소서를 회사명·일부 토큰만 바꿔 따라 쓰지 마라(가장 흔한 실패).**
22
25
  4. **초안 작성:** 항목별 **소제목** + **두괄식**(핵심 결론 먼저) → **STAR-lite**(S/T 짧게, A에 무게, R=전후 델타 정량) → **직무/인재상 명시 연결**. 회사 고유 근거 1~3문장("회사명 placeholder로 바꾸면 어색"이어야 함). 글자수 **상한의 ~70%+를 채우되**, 입사후포부는 앞 문단과 **다른 새 기여**로(같은 성과·표현 반복 금지).
23
26
  - **STAR 앵커 우선(사실 변경 금지·표현 강화 허용):** Action/Result는 저장된 '한 일'(도입·적용·운영·분석한 것)과 저장된 결과에 앵커한다. 저장된 행위·맥락은 풍부하게 서술해도 좋다(평가자가 중시하는 경험 서술형 환영). **금지는 발명뿐** — 저장 근거가 없는 진단 결과·방법/도구·원인 규명·실패율·문서화·후속 성과를 새로 지어내지 마라. 저장에 없는 과정을 꼭 암시해야 하면 "기록/근거로 범위를 좁혔다" 수준의 중립 한 구절까지만. <!-- applied: P4-star-anchor / closed 2026-06-20 — codex 규칙-텍스트 전용 표결 agree(4/4) + claude 3모델 만장일치 + R7 산출물 PASS 간접지지(cl-s1=94 opus생성·codex채점 / cl-s2 codex생성 90 PASS) -->
24
27
  - **프레이밍 경계(작성·평가 공통):** 일반 도메인 원리는 *후보 귀속 없이* 맥락으로 OK. 비즈니스 임팩트·문제 프레이밍은 *저장된 성과/지표가 암시하는 범위 안*에서만. **(아래 예시는 IT 직군 하나의 예일 뿐 — 원리는 전 직군 동일: 영업=매출/전환 기록, 운영·생산=처리량/불량률 기록, 마케팅=유입/전환 기록 등.)** 예: p99 개선 기록 → "느린 구간을 좁혀" OK / 기록 없는 "결제 경험을 해치던" 단정 금지. 사고·이유 귀속은 *저장된 결정·행동이 논리적으로 함의할 때만*(예: "Kafka 보류 결정" 저장 → "복잡도 대비 효과가 적어 보류" OK / "새 기술보다 ~가 낫다고 봤다"식 새 판단 삽입 금지). **금지:** 후보가 측정·관찰·확인했다는 행위, 출처에 없는 중간 지표(A/B를 "클릭"으로 특정), 출처에 없는 파생 정확수치(%·배수) 단정(저장 전후값은 그대로 쓰거나 "절반 이하로" 같은 정성 표현만 허용), 진단 결과·원인·감정·의도를 후보의 사실로 단정. **수치·고유명사는 저장 데이터의 범위·채널·주체 그대로 쓴다('캠페인 누적'을 '인스타그램 누적'으로 좁히듯 채널/주체 재귀속 금지). 학습·기초만 저장된 것을 수행으로 승격하지 마라('SQL 기초 학습'+'주간 리포트 작성'→"SQL로 데이터를 추출해 리포트 작성"식 연결 단정 금지).** <!-- applied: fact-anchor-boundary / closed 2026-06-20 — codex 규칙-텍스트 전용 표결 agree(4/4) + claude 3모델 만장일치 + R7 경계강화 cross-vendor PASS 간접지지 -->
@@ -42,7 +45,7 @@
42
45
  내부 루브릭·도구명·이 EOP는 노출하지 않는다. 결과 저장은 `save_cover_letter_version`, 안내는 대시보드 링크.
43
46
 
44
47
  ## 5. 자가검증 루브릭 (제출 전 — research §리뷰어 루브릭)
45
- **CRITICAL(하나라도 → 재작성):** C1 회사/직무/지역 오기·교차지원 복붙 흔적 / C2 자격·필수요건 미충족·미언급 / C3 글자수(byte/char cap)·양식·필수항목 위반(**제출용 본문 블록에 결론/근거/리스크 등 메타 텍스트 혼입** 포함) / C4 날조 또는 문서 간 모순 수치(**수치·고유명사는 저장 데이터의 범위·채널·주체 그대로 쓴다 — '캠페인 누적'을 '인스타그램 누적'처럼 더 좁은 채널/주체로 재귀속(범위 변경)하거나, 저장·공고에 없는 정확수치·기간·비율('입사 후 1~2년'·%·배수)을 단정하면 C4 날조**) / C5 블라인드 위반(공공). <!-- applied: r7-cover-letter / §5 C4 채널 재귀속·파생수치 경계 (R7 cross-vendor PASS) / 2026-06-20 -->
48
+ **CRITICAL(하나라도 → 재작성):** C1 회사/직무/지역 오기·교차지원 복붙 흔적·**기존 자소서 템플릿 복제**(다른 공고인데 직무 적합 앵글·도입·산문이 직전 자소서와 거의 같아 회사명만 바뀐 수준 — *단 사실·수치·고유명사·보이스의 일관성은 정상이며 억지 변형 금지*; 문제는 프레이밍·문장의 무차별 carryover다) / C2 자격·필수요건 미충족·미언급 / C3 글자수(byte/char cap)·양식·필수항목 위반(**제출용 본문 블록에 결론/근거/리스크 등 메타 텍스트 혼입** 포함) / C4 날조 또는 문서 간 모순 수치(**수치·고유명사는 저장 데이터의 범위·채널·주체 그대로 쓴다 — '캠페인 누적'을 '인스타그램 누적'처럼 더 좁은 채널/주체로 재귀속(범위 변경)하거나, 저장·공고에 없는 정확수치·기간·비율('입사 후 1~2년'·%·배수)을 단정하면 C4 날조**) / C5 블라인드 위반(공공). <!-- applied: r7-cover-letter / §5 C4 채널 재귀속·파생수치 경계 (R7 cross-vendor PASS) / 2026-06-20 -->
46
49
  **MAJOR:** M1 제너릭·비고유 지원동기(why-this-company 증명 없음) / M2 핵심 성과에 정량 결과 없음(노력어 의존) / M3 경험을 요구 역량/직무에 미연결(문항 의도 놓침) / M4 두괄식·소제목 약함 / M5 빈 형용사 슬롭·AI 균일 / M6 입사후포부 과대·사업방향 무관.
47
50
  **MINOR:** 오탈자·맞춤법 / 상한 대비 과소·과패딩 / 수동·약한 동사·"나" 관점(영문) / 항목 중복·불균등 가중 / (영문) 수신자 결함.
48
51
  **통과 = CRITICAL 0 + MAJOR 0 + 슬롭/제너릭 없음 + 모든 수치가 저장 데이터에 앵커.**
@@ -11,7 +11,7 @@
11
11
  - 전이가능 스킬은 **자기주장이 아니라 행동 증거**로만 인정.
12
12
  - 과적합(overqualified)은 자동 탈락이 아니라 **리스크+완화질문**으로 표시.
13
13
  - 전역 직감 점수 금지 — **요건별로 증거를 인용하고 강도 등급**을 매긴다(구조화 채점이 인상 채점을 이긴다).
14
- - 높은 키워드 커버리지를 높은 적합으로 등치하지 않는다(>75~80%는 스터핑 신호).
14
+ - 높은 키워드 커버리지를 높은 적합으로 등치하지 않는다(여기 커버리지=*이력서의 JD 키워드 밀도*, >75~80%는 스터핑 신호 — [job-analysis](job-analysis.md) §0의 요건 *충족 %*와는 다른 축이며 둘 다 합격 게이트가 아니다).
15
15
 
16
16
  ## 0a. 발명 차단 — 사실 span 앵커 (판정영향 주장만)
17
17
  <!-- applied: r12-fit-contract-invention / 2026-06-21 (consensus R3 전원 agree; 결함-발생률 게이트) -->
@@ -14,7 +14,7 @@
14
14
  - **확신도 분리:** 원문 인용=확정, 그 외는 '추정'이며 확신도를 낮춰 표현한다(예: "이메일 접수형"은 확정, "수시 채용"은 낮은 확신의 추론). **외부 분류 코드(NCS 번호 등)는 데이터로 확정될 때만** 쓰고 아니면 계열명만(코드 발명 금지). 산출물에 'EOP'·내부 기준명을 노출하지 않는다.
15
15
 
16
16
  ## 1. 입력 수집
17
- 공고 텍스트(`get_application_context`의 job, 또는 사용자가 준 경로를 `read_document`/본문 붙여넣기). 회사·플랫폼 맥락.
17
+ 공고 텍스트(`get_application_context`의 job, 또는 사용자가 준 경로를 `read_document`/본문 붙여넣기). **공고가 URL(링크)이면 CareerMate가 가져오지 않으니, 네 브라우징·웹 도구로 본문을 직접 읽어 분석하고 `save_job_posting`에 텍스트로 저장한다(웹 도구가 없으면 사용자에게 본문을 붙여넣어 달라고 요청).** 회사·플랫폼 맥락.
18
18
 
19
19
  ## 2. 분석 절차
20
20
  1. **세그먼트:** 직무내용 / 자격요건(필수) / 우대사항 / 처우·복지 / 회사소개.
@@ -40,6 +40,7 @@
40
40
  - '성실한·책임감·열정·소통을 잘하는 / 꼼꼼한·끈기있는·헌신적인·주도적인 / hard-working·team player·detail-oriented·motivated·proactive·dedicated·diligent·people person' 같은 **증거 없는 평가형 형용사**나 그 동의어로 자신을 묘사하지 마라.
41
41
  - '~라고 생각합니다', '많은 것을 배웠습니다', '~을 통해 성장했습니다', '귀사' 같은 증거 없는 자기서술 종결구문을 남발하지 마라.
42
42
  - 이력서 불릿을 문장으로 바꿔 그대로 반복하지 마라.
43
+ - **다른 공고에 낸 자소서를 회사명·일부 토큰만 바꿔 재사용하지 마라(자기복제).** 사실·수치·보이스는 공고 간 일관되게 두되(억지 변형 금지), 직무 적합 앵글·도입·산문은 공고마다 새로 짠다. 자기복제는 C1 위반이고, 한국은 표절검사(무하유·Copy Killer)·AI 탐지에서 자기표절로 걸린다.
43
44
  - 회사명을 바꿔도 말이 되는 제너릭 지원동기를 쓰지 마라.
44
45
  - 한 통/한 문항에 5~6개 경험을 얕게 나열하지 마라(1~2개 깊게).
45
46
  - '규모'(담당 규모·팀 인원·연도)를 '성과'로 위장하지 마라 — 본인 행동의 변화량이 결과다.
@@ -63,11 +63,11 @@
63
63
  | ID | 검사 항목 | 합격 기준 | 측정 방법(LLM 없이 셀 수 있게) |
64
64
  |----|-----------|-----------|--------------------------------|
65
65
  | R1 | 모든 요건에 [필수/우대] 라벨 + 복합 요건 분해 | 라벨 누락 0개; 쉼표·'및'·'and/or'로 묶인 줄이 개별 항목으로 분리됨 | 추출 항목 수를 세고 각 항목에 'must'/'nice' 토큰 존재 확인(누락==0). 원문에서 구분자 포함 줄 수 대비 분해 항목 수가 ≥ 구분자+1 인지 검사 |
66
- | R2 | 충족·부분충족 판정에 출처 ID + evidence 존재 | status가 met·partial인데 evidence[] 배열이 빈 항목 0개 | (AI 셀프체크 기준으로 유효; **서버 강제는 Phase B** — 현재 save_fit_analysis 스키마에 requirements[]/evidence 필드 없음) met·partial 항목 수 대비 `evidence.length>0` 항목 수 비율 == 100% |
67
- | R3 | quotedBullet/quotedMetric이 원문 substring (환각·수치변형 차단) | 인용 문자열이 해당 sourceId 입력 텍스트의 부분문자열이 아닌 항목 0개 | (AI 셀프체크 기준으로 유효; **서버 강제는 Phase B** — 입력 구조 부재, 현재 연결 AI가 substring 수동 대조) 각 evidence의 quotedBullet을 입력 이력서/경력 텍스트에 대해 indexOf/`includes` 문자열 검색 → 미발견 개수 == 0 |
66
+ | R2 | 충족·부분충족 판정에 출처 ID + evidence 존재 | status가 met·partial인데 evidence[] 배열이 빈 항목 0개 | (AI 셀프체크 — 서버 자동검사는 아직 없음) met·partial 항목 수 대비 `evidence.length>0` 항목 수 비율 == 100% |
67
+ | R3 | quotedBullet/quotedMetric이 원문 substring (환각·수치변형 차단) | 인용 문자열이 해당 sourceId 입력 텍스트의 부분문자열이 아닌 항목 0개 | (AI 셀프체크 — 서버 자동검사는 아직 없음) 각 evidence의 quotedBullet을 입력 이력서/경력 텍스트에 대해 indexOf/`includes` 문자열 검색 → 미발견 개수 == 0 |
68
68
  | R4 | gap 표기 + must-have 미충족마다 완화 전략 | 미충족 must 중 mitigation 문장이 없는 항목 0개 | 미충족 must 개수 == mitigation 문장 개수 비교 |
69
69
  | R5 | must-have 충족률 수치 제시 + partial 제외 규칙 | 'met must 수 / 전체 must 수' 분수·% 1개 이상; 분자에 partial 미포함 | 분수/% 정규식(예: `\d+/\d+`, `\d+%`)이 must 문맥과 함께 1회 이상; 분자 == status=='met' 개수와 일치 확인 |
70
- | R6 | critical·hardGate gap 시 '권장 불가' 병기 (게이트 단독 우선) | hardGate 또는 critical이 gap인데 verdict가 'recommend'인 사례 0개 | (AI 셀프체크 기준으로 유효; **서버 강제는 Phase B** — verdict/requirements 필드 부재, 현재 연결 AI가 수동으로 '권장 불가' 강제) `requirements`에 `hardGate&&status==gap` 또는 `critical&&status==gap`이 1개라도 있으면 `verdict=='recommend'` 금지 |
70
+ | R6 | critical·hardGate gap 시 '권장 불가' 병기 (게이트 단독 우선) | hardGate 또는 critical이 gap인데 verdict가 'recommend'인 사례 0개 | (AI 셀프체크 — 서버 자동검사는 아직 없음) `requirements`에 `hardGate&&status==gap` 또는 `critical&&status==gap`이 1개라도 있으면 `verdict=='recommend'` 금지 |
71
71
  | R7 | 키워드 표면 존재가 의미 매칭과 분리 보고 | JD 핵심 키워드별 'O/X' 표기 1개 이상, 의미 매칭 충족 판정과 별도 필드 | 출력에 `keywordSurfacePresent` 또는 'O/X·있음/없음' 항목 카운트 ≥ 1 |
72
72
  | R8 | partial 남용 방지 (사유 강제) | partial 항목 중 '왜 full이 아닌지' 사유 문장이 없는 항목 0개 | partial 항목 수 == partialReason 문장 수 비교 |
73
73
  | R9 | gap 과소평가(false-negative) 방지 | gap으로 분류한 must마다 인접 증거 탐색 시도 흔적 존재 | gap must 개수 대비 `transferAttempt` 표기(예: "인접 검토: …") 항목 수 == 100% |
@@ -2,6 +2,8 @@
2
2
 
3
3
  > 이 문서는 CareerMate가 **연결된 AI에게 제공(serve)** 하는 지식이다. CareerMate 안에는 LLM이 없다 — 분석·작성·검증은 연결된 AI가 수행하고, 이 플레이북·루브릭은 그 AI가 **세계 최고 수준**으로 일하도록 안내한다.
4
4
 
5
+ > **역할 경계:** 이 문서는 글을 **쓰는·생성하는(write)** 단계의 플레이북이다(`get_playbook`, 작성 직전). 다 쓴 초안을 **저장 직전 점검·채점**하는 일은 짝 검증기 [`verifiers/human-voice.md`](verifiers/human-voice.md)(`get_verifier`)가 맡는다 — 같은 주제라도 *쓰기 지침*과 *검증 루브릭*은 역할이 다르고, 두 곳의 강도 차이(예: 사실 보존이 여기선 LLM-플래그, 검증기 C8·C9에선 하드 게이트)는 **의도된 것**이다. 두 문서는 슬롭·과장·클리셰·번역투 사전을 **의도적으로 각자 온전히 복제**해 들고 있다 — 한쪽만 호출돼도 사전이 온전하도록 함이니, **단일 출처로 합치거나 다른 문서 포인터로 대체하지 마라(독립 serve가 깨진다).**
6
+
5
7
  ## 1. 무엇을 하는가
6
8
 
7
9
  커리어 문서(자기소개서·자기 PR·지원 메일·면접 답변)를 "AI가 쓴 티" 없이, 사용자 고유의 목소리로, 사실을 보존하며 쓰도록 연결된 AI를 안내한다. CareerMate의 **1차 사용자층은 한국어 자소서 작성자**이므로 이 플레이북은 한국어 슬롭(번역투·클리셰·기계적 병렬·수동태)을 1차 기준으로, 영어 마커는 영문 지원서일 때만 적용하는 **보조 기준**으로 둔다. 이 문서는 기존 `packages/prompts/src/humanize.ts`의 정성 가이드(한국어 8개 항목, `epoko77-ai/im-not-ai` 근거)를 **상위 단일 원본으로 흡수·확장**하고, 거기에 "LLM 없이 셀 수 있는" 결정론 루브릭과 채용 적합성 축을 더한 것이다. 핵심은 슬롭 제거가 아니라 **"거짓 없고 직무에 맞닿은, 사람 목소리의 글"**을 만드는 것이다 — 슬롭이 0이어도 동문서답·내용 공백이면 떨어진다.
@@ -25,7 +25,7 @@
25
25
  | C5 | 이미지·아이콘·스킬바 부재 | 그래픽 요소 0개 | 이미지 마크업(`![]`, `<img`)·이모지/심볼로 표기된 연락처 아이콘·"skill bar" 채움문자(예: ●●●○○, █) 출현 횟수 **= 0** |
26
26
  | C6 | 연락처가 본문 상단에 위치 | 이메일·전화가 헤더/푸터가 아닌 본문 첫 영역에 존재 | 본문 텍스트 첫 15% 구간(또는 첫 5줄) 안에서 이메일 정규식(`[\w.+-]+@[\w-]+\.[\w.]+`) 매치 **≥ 1** 그리고 전화 패턴 매치 **≥ 1** |
27
27
  | C7 | 일관된 날짜 형식 | 경력/학력 기간이 ATS가 인식하는 형식으로 통일 | 날짜 토큰을 추출해 허용 패턴(`MMM YYYY`, `Month YYYY`, `MM/YYYY`, `YYYY.MM`, `YYYY-MM`) 중 **단일 패턴 비율 ≥ 90%**. 혼용·"현재/Present" 외 자유서술 날짜 비율 < 10% |
28
- | C8 *(advisory)* | JD 하드스킬 키워드 커버리지 | JD 상위 키워드의 충분 비율을 본문이 포함 | 산출물 본문에 존재하는 키워드 개수 M / JD 추출 키워드 N → **M/N ≥ 0.70 = 권장 출발점(advisory·비차단; 0.70은 법칙이 아니라 출발점이며 AI가 JD 규모·역할 수에 맞게 조정)**. 바로 옆 75%+(Jobscan 권장)/80%(스위트스폿)는 **비구속 상류 참조치 — 제2 임계값 아님**(단일 합격선은 0.70 하나). 매칭은 raw substring이 아니라 **외부 AI가 원문 JD·이력서 텍스트를 정규화·동의어 묶음(JD 요구사항 자체는 불변)한 토큰 집합을 토큰 경계로** 센다(raw substring 금지; ats.md R4 동일 원본; `Java`⊂`JavaScript` 거짓양성 차단). JD 미제공 시 N/A 처리하고 사유 기록 |
28
+ | C8 *(advisory)* | JD 하드스킬 키워드 커버리지 | JD 상위 키워드의 충분 비율을 본문이 포함 | 산출물 본문에 존재하는 키워드 개수 M / JD 추출 키워드 N → **M/N ≥ 0.70 = 권장 출발점(advisory·비차단; 0.70은 법칙이 아니라 출발점이며 AI가 JD 규모·역할 수에 맞게 조정)**. 바로 옆 75%+(Jobscan 권장)/80%(스위트스폿)는 **비구속 상류 참조치 — 제2 임계값 아님**(단일 합격선은 0.70 하나). 매칭은 raw substring이 아니라 **외부 AI가 원문 JD·이력서 텍스트를 정규화·동의어 묶음(JD 요구사항 자체는 불변)한 토큰 집합을 토큰 경계로** 센다(raw substring 금지; ats.md R4 동일 원본; `Java`⊂`JavaScript` 거짓양성 차단). JD 미제공 시 N/A 처리하고 사유 기록. **축 구분(메타):** 이 C8 = JD 키워드 *표면 존재(presence)* M/N≥0.70 **advisory**. `responsiveness-on-target` C8 = JD *필수요건 응답(response)* 커버리지 ≥80% **critical** — **다른 축**이라 한 자소서가 한쪽 통과·다른 쪽 미달이어도 **충돌이 아니다**(키워드가 표면에 있어도 요건에 *응답*하지 않을 수 있고, 요건에 응답해도 ATS 표면 키워드가 모자랄 수 있다) |
29
29
  | C9 | 직무명(target job title) 일치 | JD의 핵심 직무명이 본문에 등장 | JD 헤드라인 직무명을 본문(이력서 요약/타이틀, LinkedIn 헤드라인)에서 정확/근접 일치로 탐색 → 매치 **≥ 1** |
30
30
  | C10 | 키워드 스터핑 방지(과최적화) | 특정 키워드 반복이 과하지 않음 | C8 키워드 각각의 본문 등장 횟수 중 **어떤 단일 키워드도 6회 초과 금지**, 그리고 키워드 토큰이 전체 토큰에서 차지하는 비율 **< 6%**. 위반 키워드 개수 = 0 목표 |
31
31
  | C11 | 약어·풀네임 병기 | 핵심 약어가 풀네임과 함께 1회 이상 병기 | JD/본문 공통 약어(예: ATS, SQL, PM) 중 본문에 약어만 단독 등장하고 풀네임 동반이 0인 항목 개수 → **0 목표**(파서/사람 양쪽 매칭 보장) |
@@ -2,6 +2,8 @@
2
2
 
3
3
  > 이 검증기는 연결된 AI가 커리어 산출물(이력서·자소서·면접 답변·LinkedIn 등)을 점검할 때 적용하는 **크리틱 루브릭**이다. CareerMate는 루브릭을 제공하고, 실제 검사는 연결된 AI가 수행한다(내부 LLM 없음).
4
4
 
5
+ > **역할 경계:** 이 문서는 다 쓴 초안을 **저장 직전 점검·채점하는(verify)** 크리틱 루브릭이다(`get_verifier`, 저장 직전). 글을 **쓰는·생성하는** 단계의 지침은 짝 플레이북 [`../human-writing.md`](../human-writing.md)(`get_playbook`)에 있다 — *검증 루브릭*과 *쓰기 지침*은 역할이 다르고, 두 곳의 강도 차이(예: 사실 보존이 여기 C8·C9에선 하드 게이트, 플레이북에선 LLM-플래그)는 **의도된 것**이다. 두 문서의 사전은 **의도적으로 각자 온전히 복제**돼 있다(독립 호출 안전) — **단일 출처화·포인터 대체 금지(한쪽만 불려오면 사전이 사라진다).**
6
+
5
7
  ## 1. 목적
6
8
 
7
9
  이 검증기는 커리어 산출물이 **"AI가 쓴 티"가 나지 않고 사람의 목소리로 읽히는지**를 보장한다. 채용 담당자는 천편일률적인 자동 생성 글을 즉시 알아채고 신뢰를 거둔다. 구체적으로 다음 네 가지를 본다.
@@ -34,7 +34,7 @@
34
34
  | C5 | 형식 토큰 충족 | 문항이 명시한 형식 요소가 모두 존재. 예: STAR 면접이면 Situation·Task·Action·Result 4요소, "구체적 사례" 요구면 사례 1건+ | det/D · 형식 체크리스트별 대응 토큰/구간 존재 카운트 → 누락 요소 수 = 0 |
35
35
  | C6 | 글자 수/분량 준수 | 자소서 문항의 **글자 수 상·하한**을 지킴(한국 공채 자사양식의 핵심 탈락 사유). 범위 내 | det/D · 응답 길이(공백 포함/제외 둘 다) vs 문항 제한. 초과·미달 글자 수 출력 |
36
36
  | C7 | 질문 직접 응답성 | 응답이 질문에 **직접** 답하고 우회·일반론으로 빠지지 않음(off-target/회피 0건) | ai/H · 연결 AI가 "이 응답이 이 질문에 답하는가"를 Y/N 판정. N이면 빗나간 지점 명시 |
37
- | C8 | JD 필수 요건 커버리지 | JD의 **필수(required) 요건** 항목 중 산출물(이력서+자소서)에서 다뤄진 비율 **≥ 80%**, 필수 요건 미커버 **0개**. (이 ≥80%는 응답성 커버리지 신호이며 적합(fit) 권장 게이트가 아니다 — 적합 판정은 `fit-matching`/`fit-analysis`가 하드요건(knockout)으로 내린다. C8 자체는 응답성 루프 내 critical 검사로, 미달은 적합·지원을 차단하는 게 아니라 §3 보강 재작성을 트리거한다.) | det/D · JD에서 필수/우대 요건 분리 추출 → 각 요건 키워드가 산출물에 존재하는지 매칭 → (커버 요건 ÷ 전체 필수). 미커버 필수 요건 나열 |
37
+ | C8 | JD 필수 요건 커버리지 | JD의 **필수(required) 요건** 항목 중 산출물(이력서+자소서)에서 다뤄진 비율 **≥ 80%**, 필수 요건 미커버 **0개**. (이 ≥80%는 응답성 커버리지 신호이며 적합(fit) 권장 게이트가 아니다 — 적합 판정은 `fit-matching`/`fit-analysis`가 하드요건(knockout)으로 내린다. C8 자체는 응답성 루프 내 critical 검사로, 미달은 적합·지원을 차단하는 게 아니라 §3 보강 재작성을 트리거한다.) | det/D · JD에서 필수/우대 요건 분리 추출 → 각 요건 키워드가 산출물에 존재하는지 매칭 → (커버 요건 ÷ 전체 필수). 미커버 필수 요건 나열. **축 구분(메타):** 이 C8 = JD *필수요건 응답(response)* 커버리지 ≥80% **critical**. `ats-compat` C8 = JD 키워드 *표면 존재(presence)* M/N≥0.70 **advisory** — **다른 축**이라 두 게이트의 숫자·방향이 달라도 **충돌이 아니다**(요건에 응답해도 ATS 표면 키워드가 모자랄 수 있고, 키워드가 표면에 있어도 요건에 응답 안 할 수 있다) |
38
38
  | C9 | JD 우대 요건 커버리지 | **우대(preferred)** 요건 커버 비율 보고(차단 아님, 경고). 권장 ≥ 50% | det/D · C8과 동일 로직을 우대 요건에 적용. 비율만 리포트, 미달은 warning |
39
39
  | C10 | 과잉/딴소리 비율 | 응답 중 질문과 무관한 문장(Q-keyword·directive 어느 것과도 무관) 비율 **< 20%** | det/D + ai/H · 각 문장을 질문 관련/무관으로 태깅(키워드 매칭=det, 환언 무관 판정=ai) → 무관 문장 비율 |
40
40
  | C11 | 질문 전제·제약 준수 | 질문이 건 제약(예: "**팀** 경험", "**실패** 사례", "최근 3년 내")을 응답이 위반하지 않음 | ai/H · 질문의 한정어(팀/개인, 성공/실패, 기간) 추출 → 응답 사례가 그 한정과 일치하는지 판정. 위반 사례 개수 = 0 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careermate",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "type": "module",
5
5
  "description": "CareerMate — an MCP-first, local-first career workspace. Connect it to a local AI agent (Claude Desktop, Claude Code, Codex); CareerMate stores your career data locally on your machine and exposes it over MCP.",
6
6
  "license": "MIT",
@@ -66,7 +66,7 @@
66
66
  "shots:seed": "node --no-warnings --experimental-sqlite --import tsx scripts/shots-seed.ts",
67
67
  "shots": "node --no-warnings --experimental-sqlite --import tsx scripts/shots.ts",
68
68
  "doctor": "node --no-warnings --experimental-sqlite --import tsx scripts/doctor.ts",
69
- "test": "node --no-warnings scripts/check-i18n-keys.mjs && node --no-warnings scripts/run.mjs scripts/test-i18n.ts \"I18N_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-init.ts \"INIT_TEST_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-bridge.ts \"BRIDGE_TEST_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test.ts \"TEST_VERDICT PASS\"",
69
+ "test": "node --no-warnings scripts/check-i18n-keys.mjs && node --no-warnings scripts/run.mjs scripts/test-i18n.ts \"I18N_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-init.ts \"INIT_TEST_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-bridge.ts \"BRIDGE_TEST_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-verify.ts \"VERIFY_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-parsers.ts \"PARSERS_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test-migrate.ts \"MIGRATE_VERDICT PASS\" && node --no-warnings scripts/run.mjs scripts/test.ts \"TEST_VERDICT PASS\"",
70
70
  "test:ui": "node --no-warnings scripts/run.mjs scripts/ui-smoke.ts \"UI_VERDICT PASS\"",
71
71
  "typecheck": "tsc --noEmit"
72
72
  },
@@ -115,10 +115,13 @@ export const CAREER_ROUTES: Record<string, CareerRoute> = {
115
115
  verifierSequence: ['truthfulness', 'consistency', 'responsiveness-on-target'],
116
116
  loop: 'draft_verify_revise',
117
117
  },
118
+ // R22(audit): responsiveness-on-target는 한국 문항형 자소서 전용 검증기(문항별 1:1 응답·
119
+ // 글자수 상·하한 = "핵심 탈락 사유")인데 analyze_job에만 라우팅돼 있어 정작 자소서 자가검증
120
+ // 루프에서 빠져 있었다. 동문서답·미응답 문항·글자수 초과를 잡도록 write_cover_letter에 추가.
118
121
  write_cover_letter: {
119
122
  eop: 'cover-letter',
120
123
  expertSequence: ['cover-letter', 'human-writing', 'company-research'],
121
- verifierSequence: ['human-voice', 'truthfulness', 'ats-compat', 'consistency'],
124
+ verifierSequence: ['human-voice', 'truthfulness', 'responsiveness-on-target', 'ats-compat', 'consistency'],
122
125
  loop: 'draft_verify_revise',
123
126
  },
124
127
  write_career_description: {
@@ -130,7 +133,9 @@ export const CAREER_ROUTES: Record<string, CareerRoute> = {
130
133
  prepare_interview: {
131
134
  eop: 'interview-prep',
132
135
  expertSequence: ['recruiter-screen', 'interview-behavioral', 'interview-technical', 'company-research'],
133
- verifierSequence: ['truthfulness', 'consistency'],
136
+ // R22(audit): recency-staleness 추가 — 면접의 "최근 소식"·회사 근황이 낡으면 거짓 신선도가
137
+ // 되므로(company-research) 저장 전 신선도 점검을 푸시한다.
138
+ verifierSequence: ['truthfulness', 'consistency', 'recency-staleness'],
134
139
  loop: 'draft_verify_revise',
135
140
  },
136
141
  // R7 A2: manage_application_status had no route → rejection-triage / offer-evaluation /
@@ -152,7 +157,9 @@ export const CAREER_ROUTES: Record<string, CareerRoute> = {
152
157
  'onboarding-first-90-days',
153
158
  'networking-referrals',
154
159
  ],
155
- verifierSequence: ['truthfulness', 'consistency'],
160
+ // R22(audit): recency-staleness 추가 — 연봉 벤치마크·오퍼 비교는 시점이 지나면 낡은 기준이
161
+ // 되므로(salary-negotiation/offer-evaluation) 저장 전 신선도 점검을 푸시한다.
162
+ verifierSequence: ['truthfulness', 'consistency', 'recency-staleness'],
156
163
  },
157
164
  // R7 후속: linkedin-profile·portfolio는 onboarding(이력서 임포트)과 단계 의미가
158
165
  // 달라(개인 브랜드 자산 *생성*) 전용 워크플로우로 분리(consensus 권고). networking은
@@ -223,7 +223,7 @@ export const TOOLS: ToolDef[] = [
223
223
  const files = listInboxFiles(dir);
224
224
  const list = files.length ? files.map((f) => `- ${f.name}`).join('\n') : '(아직 비어 있음)';
225
225
  return ok(
226
- `문서 인입 폴더를 열었습니다:\n${dir}\n\n여기에 이력서·경력기술서·포트폴리오·기존 자기소개서 파일을 넣고 "다 넣었어"라고 알려주세요. (hwp/hwpx/docx는 자동으로 텍스트를 추출하고, pdf·이미지는 제가 직접 읽습니다.)\n\n현재 파일:\n${list}`,
226
+ `문서 인입 폴더를 열었습니다:\n${dir}\n\n여기에 이력서·경력기술서·포트폴리오·기존 자기소개서 파일을 넣고 "다 넣었어"라고 알려주세요. (hwp·hwpx·docx·pdf는 자동으로 텍스트를 추출하고, 텍스트 레이어가 없는 이미지·스캔 PDF만 제가 직접 읽습니다.)\n\n현재 파일:\n${list}`,
227
227
  { dir, files },
228
228
  );
229
229
  },
@@ -232,7 +232,7 @@ export const TOOLS: ToolDef[] = [
232
232
  name: 'read_inbox',
233
233
  title: '인입 폴더 파일 읽기',
234
234
  description:
235
- '인입 폴더(~/.careermate/inbox)에 사용자가 넣어 둔 문서들의 본문을 한 번에 읽어 돌려줍니다. open_inbox로 폴더를 안내한 뒤 사용자가 "다 넣었어"라고 하면 호출하세요. docx/hwp/hwpx/텍스트는 텍스트를 추출하고, pdf·이미지는 추출하지 않고 파일 경로를 돌려주니 AI 클라이언트의 파일 읽기 기능으로 그 경로를 직접 열어 읽으세요. 읽은 내용을 구조화해 프로필은 save_profile, 이력서·경력기술서·포트폴리오는 add_resume, 기존 자기소개서는 save_cover_letter_version으로 저장하세요(파일 내용을 새 파일로 다시 쓰지 마세요). filename을 주면 해당 파일만 읽습니다.',
235
+ '인입 폴더(~/.careermate/inbox)에 사용자가 넣어 둔 문서들의 본문을 한 번에 읽어 돌려줍니다. open_inbox로 폴더를 안내한 뒤 사용자가 "다 넣었어"라고 하면 호출하세요. docx·hwp·hwpx·pdf·텍스트(.txt/.md)에서 본문 텍스트를 추출해 돌려줍니다(read_document와 동일한 추출 스택). 단 텍스트 레이어가 없는 이미지·스캔 PDF는 추출되지 않아(unsupported=true) 파일 경로만 돌려주니, 그 경로는 AI 클라이언트의 파일 읽기 기능으로 직접 열어 읽으세요. 읽은 내용을 구조화해 프로필은 save_profile, 이력서·경력기술서·포트폴리오는 add_resume, 기존 자기소개서는 save_cover_letter_version으로 저장하세요(파일 내용을 새 파일로 다시 쓰지 마세요). filename을 주면 해당 파일만 읽습니다.',
236
236
  inputSchema: {
237
237
  filename: z.string().optional().describe('특정 파일만 읽을 때 파일명(대소문자 무시). 생략하면 폴더 안 모든 파일을 읽습니다.'),
238
238
  max_chars: z.number().optional().describe('파일별 반환 텍스트 최대 길이(초과 시 잘라냄). 기본 20000자'),
@@ -348,7 +348,7 @@ export const TOOLS: ToolDef[] = [
348
348
  name: 'add_resume',
349
349
  title: '이력서/경력기술서 추가',
350
350
  description:
351
- '이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트 또는 AI가 전문가 절차로 정리한 Markdown을 넣으세요. 경력기술서를 작성해 저장할 때는 kind=career_description, source=ai를 사용합니다. kind 기본값은 resume입니다. is_primary=true로 대표 문서를 지정하면 get_application_context의 primary_resume로 노출됩니다.',
351
+ '이력서, 경력기술서, 포트폴리오 등 문서를 저장합니다. content에는 사용자가 업로드/붙여넣은 텍스트 또는 AI가 전문가 절차로 정리한 Markdown을 넣으세요. 경력기술서를 작성해 저장할 때는 kind=career_description, source=ai를 사용합니다. ⚠️ content에는 이 문서 한 종류의 본문만 넣고, 자기소개서 등 다른 종류 문서를 같은 content에 이어붙이지 마세요 — 자기소개서는 save_cover_letter_version으로 따로 저장합니다(한 번에 둘 다 요청받았어도 한 저장 호출에 두 문서를 합치지 않습니다). kind 기본값은 resume입니다. is_primary=true로 대표 문서를 지정하면 get_application_context의 primary_resume로 노출됩니다.',
352
352
  // Reuse the shared (length-bounded, whitespace-rejecting) Document schema so
353
353
  // the MCP ingestion path — the primary route for résumé text — inherits the
354
354
  // same caps as the web API. kind stays optional here (defaults to resume).
@@ -536,7 +536,7 @@ export const TOOLS: ToolDef[] = [
536
536
  name: 'save_cover_letter_version',
537
537
  title: '자기소개서 버전 저장',
538
538
  description:
539
- '자기소개서의 새 버전을 저장합니다. CareerMate 자소서 작성 워크플로우의 핵심 저장 단계입니다. cover_letter_id를 주면 기존 자소서에 새 버전을 추가하고, 없으면 새 자소서를 만들어 v1로 저장합니다(이때 title 권장). note에 이 버전의 변경 요약(예: "지원동기 보강")을 남기면 사용자가 대시보드에서 버전 히스토리를 이해하기 쉽습니다. job_id로 공고에 연결하면 지원 항목과 자동 연결됩니다. 저장 직전 자동으로 "근거 없는 수치" 점검을 돌립니다 — 본문의 정량 수치가 저장된 경력·이력서·프로젝트에 근거가 없으면(환각 의심) 저장이 막힙니다. 그때는 원본의 실제 수치로 고치거나, 의도한 값이면 force:true로 다시 저장하세요.',
539
+ '자기소개서의 새 버전을 저장합니다. CareerMate 자소서 작성 워크플로우의 핵심 저장 단계입니다. ⚠️ content에는 자기소개서 본문만 넣고, 경력기술서 등 다른 종류 문서를 이어붙이지 마세요(경력기술서는 add_resume(kind=career_description)로 따로 저장). cover_letter_id를 주면 기존 자소서에 새 버전을 추가하고, 없으면 새 자소서를 만들어 v1로 저장합니다(이때 title 권장). note에 이 버전의 변경 요약(예: "지원동기 보강")을 남기면 사용자가 대시보드에서 버전 히스토리를 이해하기 쉽습니다. job_id로 공고에 연결하면 지원 항목과 자동 연결됩니다. 저장 직전 자동으로 "근거 없는 수치" 점검을 돌립니다 — 본문의 정량 수치가 저장된 경력·이력서·프로젝트에 근거가 없으면(환각 의심) 저장이 막힙니다. 그때는 원본의 실제 수치로 고치거나, 의도한 값이면 force:true로 다시 저장하세요.',
540
540
  inputSchema: CoverLetterVersionInputSchema.shape,
541
541
  handler: (args) => {
542
542
  try {
@@ -708,7 +708,8 @@ export const TOOLS: ToolDef[] = [
708
708
  as_of: (asOf ?? '').slice(0, 7), // 재직중=updated_at(정보 확인 시점) / 종료=end_date
709
709
  months,
710
710
  years: Math.round((months / 12) * 10) / 10,
711
- data_age_months: e.is_current ? monthsBetween(e.updated_at, today) : null,
711
+ // 음수 방지(미래 updated_at·시계 오차) — 0 미만은 0으로 클램프
712
+ data_age_months: e.is_current ? Math.max(0, monthsBetween(e.updated_at, today) ?? 0) : null,
712
713
  };
713
714
  })
714
715
  .filter((t): t is NonNullable<typeof t> => t != null);
@@ -743,7 +744,7 @@ export const TOOLS: ToolDef[] = [
743
744
  try {
744
745
  const fit = saveFitAnalysis(args);
745
746
  const suggested_next_action = {
746
- prompt: '이 공고에 맞춘 자기소개서를 써드릴까요?',
747
+ prompt: '이 공고에 맞춘 자기소개서를 써드릴까요? (회사 지정 문항·양식·글자수가 있으면 함께 알려주시면 그에 맞춰 작성합니다)',
747
748
  options: [
748
749
  {
749
750
  label: '네, 이 공고에 맞춘 자기소개서를 써줘',
@@ -763,6 +764,7 @@ export const TOOLS: ToolDef[] = [
763
764
  '사용자에게 다음 선택지를 보여주세요:',
764
765
  '1. 네, 이 공고에 맞춘 자기소개서를 써줘',
765
766
  '2. 아니요, 분석만 저장해줘',
767
+ '(1번이면, 회사 지정 문항·양식·글자수가 있으면 알려달라고 요청하세요 — 한국 자소서는 문항형이 많습니다. 없으면 일반 양식으로 진행합니다.)',
766
768
  ].join('\n'),
767
769
  {
768
770
  ...fit,
@@ -21,6 +21,7 @@ CareerMate는 사용자의 컴퓨터에서 동작하는 로컬 커리어 관리
21
21
 
22
22
  2. 결과는 반드시 다시 저장합니다.
23
23
  - 분석·작성한 내용은 머릿속이나 대화창에만 남기지 말고 해당 저장 툴로 CareerMate에 기록하세요: 공고는 \`save_job_posting\`, 적합도 분석은 \`save_fit_analysis\`, 자기소개서 버전은 \`save_cover_letter_version\`, 경력기술서는 \`add_resume\`(kind=career_description), 면접 준비는 \`save_interview_prep\`, 상태 변경은 \`update_application_status\`.
24
+ - 여러 문서를 한 번에 요청받아도 각 문서는 각자의 저장 툴로 따로 저장하세요. 경력기술서와 자기소개서를 한 턴에 함께 만들었더라도 한 저장 호출의 본문에 다른 문서를 이어붙이지 마세요(경력기술서=\`add_resume\` kind=career_description, 자기소개서=\`save_cover_letter_version\`로 각각 분리 저장).
24
25
  - 저장 후에는 무엇이 저장되었는지 사용자에게 간단히 알려주세요.
25
26
 
26
27
  3. 데이터를 지어내지 않습니다.