# 사내 문서 지식창고 + MCP 연결 — AI 실행용 재현 설계서

> **이 문서의 사용법**: 이 문서를 Claude Code(또는 코드를 다룰 수 있는 AI 에이전트)에게 통째로 주고,
> "이 설계서대로 우리 조직 버전을 만들어줘. 우리 환경은 ○○(ERP/그룹웨어 이름, 사용 PC)야"라고 지시하면 된다.
> AI는 아래 단계 순서(마일스톤 M1→M6)를 지키고, §9의 안전 불변 조건을 어떤 경우에도 우회하지 않는다.
>
> 원본 구현: HC(신용카드사회공헌재단)가 사내 그룹웨어 "아마란스10"의 결재문서 약 7,200건을
> 검색 가능한 지식창고로 만들고 MCP로 ChatGPT/Claude에 연결한 실사례(2026-06~07).
> 조직 고유 값은 `[조직별 대체]`로 표시했다.

---

## 0. 목표와 최종 산출물

**목표 한 줄**: 사내에 쌓인 결재·행정 문서를 통째로 내려받아, AI(ChatGPT·Claude)가
**원본 문서를 근거로 인용하며** 답하게 만든다. 답변 중 재사용 가치가 있는 결론은
지식노트로 축적되어 시스템이 스스로 똑똑해진다.

최종 산출물 7가지:

1. **raw 아카이브** — ERP에서 내려받은 원본(문서당 폴더: 메타 + 본문 + 첨부). 기계 생성, 사람이 손대지 않음
2. **extract 텍스트층** — 모든 문서를 마크다운 1장(doc.md)으로: 프론트매터 + 본문 + 첨부 텍스트(OCR 포함)
3. **검색 인덱스** — SQLite FTS5 전문검색 DB (한국어 부분일치용 trigram)
4. **선별 위키** — 고가치 문서만 LLM으로 구조화한 요약 카드 + 사업·규정 허브 (wiki/concepts·entities·sources·projects·synthesis)
5. **MCP 서버** — 검색/열람/첨부읽기/지식저장 도구를 노출. 권한 분리 인스턴스 2개(민감문서 차단판 / 전체판)
6. **지식노트 폴더** — 질문→답변에서 나온 결론을 근거 문서번호와 함께 저장, 다음 검색에서 우선 제시 (wiki/knowledge/)
7. **온톨로지 팩** — 특정 주제를 깊게 파야 할 때 만드는 주제별 지식 상자(선택 단계)

---

## 1. 전제 조건 (준비물)

| 항목 | 내용 |
|---|---|
| 상시 구동 PC | Mac(원본 구현) 또는 Linux. 야간 배치와 MCP 서버가 여기서 돈다 |
| Python 3.11+ | `uv`로 실행 권장 (시스템 파이썬이 낡아도 PEP723 인라인 의존성으로 우회) |
| SQLite | FTS5 지원 빌드 (macOS 기본 포함) |
| Playwright | ERP 로그인 세션 재사용(CDP 연결)용 |
| MCP 프레임워크 | FastMCP 3.x (`stateless_http` 지원 버전) |
| 외부 노출용 서버(선택) | 공개 IP를 가진 저가/무료 VM + Caddy. ChatGPT 커넥터 연결 시에만 필요 |
| Tailscale(선택) | 업무 PC와 외부 서버를 사설망으로 잇는 용도 |
| ChatGPT 유료 플랜(선택) | 커스텀 커넥터(개발자 모드)는 유료 전용. Claude도 커스텀 커넥터는 유료 플랜 |
| HWP 파싱 | `hwp5` 라이브러리(헤드리스). **한글 COM(pyhwpx)은 SSH 비대화형에서 멈추므로 서버 환경 금지** |
| OCR | macOS Vision 프레임워크(원본 구현). Linux라면 Tesseract·PaddleOCR 등으로 대체 |
| 임베딩(선택) | BGE-m3-ko + sqlite-vec. §5 참조 — 처음엔 안 만들어도 된다 |

---

## 2. 아키텍처 개요

```
[사내 ERP/그룹웨어]
   │  ① 수집 — 로그인된 크롬(CDP :9222) 재사용 + 브라우저 내 HMAC 서명
   ▼
raw/approval/{연도}/{종류}/{문서번호}_{제목}/   ← 원본. 절대 수정 금지
   │  ② 추출 — 양식형=메타JSON, 서술형=본문 재다운(refill 큐), 첨부=파서+OCR
   ▼
extract/**/doc.md   ← 문서당 마크다운 1장 (텍스트층)
   │  ③ 색인 — SQLite FTS5 trigram
   ▼
index/search.db     ← 결재문서·규정·공유자료가 kind로 구분되어 한 DB에
   │  ④ 선별 위키화 — 고가치 문서만 LLM 구조화 (전량 위키화 금지)
   ▼
wiki/{concepts,entities,sources,projects,synthesis,knowledge}/
   │  ⑤ MCP 서버 — 검색/열람/첨부/지식저장 도구 노출 (권한분리 2인스턴스)
   ▼
Tailscale 사설 IP 바인딩 → 공개서버 Caddy 시크릿 경로 → ChatGPT/Claude 커넥터
   │  ⑥ 지식노트 — 답변의 결론을 근거와 함께 저장, 검색이 우선 제시 (자가발전)
   │  ⑦ 온톨로지 팩 — 주제별 심화 지식 상자 (선택)
```

핵심 설계 사상 3가지 (이 순서로 결정됐고, 이 순서가 비용을 결정한다):

- **검색 중심, LLM은 선별에만.** 문서마다 LLM 요약(전량 위키화)은 규모가 커지면 토큰 비용이 폭증한다
  (원본 구현에서 이틀간 API 환산 약 $550 소모 사고 후 폐기). 파이썬으로 글자만 뽑아 FTS로 검색하고,
  LLM은 고가치 문서 요약과 질의응답에만 쓴다.
- **계층형(Tiered) 구조.** Tier0=전량 FTS 색인(AI 미사용) / Tier1=전량 텍스트 캐시(AI 미사용) /
  Tier2=선별 LLM 구조화. 임베딩·벡터검색은 Tier1.5로 나중에 평가 후 결정.
  (단계 번호와의 대응: Tier1=②의 extract 텍스트층, Tier0=③의 색인, Tier2=④의 선별 위키화)
- **모든 답은 근거 문서를 인용한다.** 검색 결과에 없는 내용은 "문서에서 확인 안 됨"이라고 답하게
  MCP 서버 지침(instructions)에 명문화한다. 환각 억제 3종: 인용 강제 · 없으면 모른다 · 근거 대조.
  (검색해서 읽고 근거로 답하게 하는 이 구조를 통칭 RAG라고 부른다.)

---

## 3. 단계별 구현 스펙

### ① 수집 (Collection)

**목적**: ERP의 결재문서를 문서당 폴더 하나로 내려받는다. 공식 API가 없어도 된다.

**구현 요점**
- 사용자가 로그인해 둔 크롬을 원격 디버깅 포트(`--remote-debugging-port=9222`)로 띄우고,
  Playwright `connect_over_cdp`로 그 세션에 붙는다. 새 브라우저를 열지 않는다.
- `[조직별 대체]` ERP가 요청 서명을 요구하면(원본 구현의 아마란스10은
  `HMAC-SHA256(token+transactionId+timestamp+pathname, hashKey)` 방식),
  서명 계산 JS를 **브라우저 페이지 컨텍스트 안에서 실행**해 헤더만 받아온다.
  서명 키는 브라우저 메모리 밖(파이썬/디스크)으로 꺼내지 않는다.
  키 후보×인코딩 조합을 검증 엔드포인트가 성공할 때까지 시도하는 캘리브레이션 함수를 둔다.
  대상 ERP가 서명을 요구하지 않으면 이 항목은 통째로 생략한다.
- 대용량 파일 다운로드는 서명 헤더만 받아서 파이썬 `urllib`이 직접 수행(안정성).
- 폴더트리 API → 폴더별 목록 API(연도별 페이지네이션) → 문서별 저장 순으로 순회한다. `[조직별 대체]`
- 저장 구조:
  ```
  raw/approval/{연도}/{문서종류}/{리프폴더}/{문서번호}_{제목}/
    ├ meta.json    # 목록행 + 양식정보 + 기안자 + 결재선 + outProcessData(양식 실데이터)
    ├ body.hwp     # 본문 파일 (※서술형은 빈 템플릿일 수 있음 — ②의 refill로 보완)
    ├ attach.json  # 첨부 메타
    ├ attach/      # 첨부 원본
    └ .done        # 수집완료 마커 (내용 "deleted"면 삭제문서로 영구 스킵)
  ```
- 일일 증분: 매일 밤(원본 구현 22:30) 현재 연도(1월엔 전년도 포함)만 재순회. `.done` 있으면 스킵.
- 로그인 자동 복구: 자격증명을 로컬 파일로 분리 보관하고, 세션 만료 감지 시 2단계 로그인
  (회사코드+ID → 비밀번호)을 스크립트가 수행. 로그인 직후 뜨는 확인 팝업(예: "출근 체크 하시겠습니까?")은
  **절대 "확인"을 누르지 말고** "오늘 하루 띄우지 않기"+취소로만 닫는다(실제 근태가 기록되는 버튼일 수 있음).

**완료 기준**: 표본 20건에서 meta.json·첨부 실물·`.done` 마커가 모두 생성되고, 재실행 시 스킵됨.

**함정**
- 문서번호는 연도마다 재사용될 수 있다 → 항상 **(문서번호+연도)** 조합으로 매칭.
- 첨부 다운로드 API에 파일 식별자를 빠뜨리면 서버가 전체를 zip으로 묶어 반환해 파일별 매칭이 깨질 수 있다.
- 삭제된 문서의 오류코드(원본 구현에선 "2156")를 감지해 `.done="deleted"`로 영구 스킵하지 않으면 매일 재시도 루프가 생긴다.

### ② 추출 (Extraction)

**목적**: 모든 문서를 `extract/**/doc.md` 마크다운 1장으로 만든다. 이 층이 검색과 AI 답변의 재료다.

**doc.md 데이터 계약** (모든 후속 단계가 이 포맷에 의존):
```markdown
---
doc_num: <문서번호>
title: <제목>
date: <YYYY-MM-DD>
drafter: <기안자 이름 (부서)>
template: <양식명>
kind: <문서종류>
year: <연도>
source: <추출경로: meta|hwp|pdf|ocr 등 쉼표 나열>
---
<본문 텍스트>

## 첨부파일 목록
### 첨부: <파일명>
<첨부 추출 텍스트>
```

**구현 요점**
- **양식형 문서**(근태신청·지출결의 등): 실데이터가 `meta.json`의 outProcessData(HTML 또는 JSON)에 있다.
  HTML은 태그 제거, JSON은 "key: value" 평탄화로 뽑는다. HWP 파싱 불필요.
- **서술형 문서**(일반기안·공문 등): 수집된 body.hwp가 **빈 양식 템플릿**인 경우가 많다
  (ERP가 양식 틀만 내려주는 구조). 본문이 확보 안 된 문서는 `needs_refill.csv` 큐에 등록하고,
  ERP 뷰어의 "PC저장(비배포용)" 기능으로 실내용 PDF를 재다운로드하는 **refill 배치**가 채운다.
  - refill은 **비파괴**: 기존 파일을 덮지 않고 새 파일명(`body_pc.pdf`)+전용 마커(`.refill_ok`)를 쓴다.
  - 일일 상한(예: 40건)과 데드라인(예: 2시간)을 걸어 야간 파이프라인에 편입한다.
- **HWP 파싱(헤드리스)**: `hwp5` 라이브러리의 XML dump를 SAX로 걸어 텍스트를 뽑는다.
  표는 셀을 ` | `, 행을 개행으로 이어 표 내용까지 보존한다. 3단 폴백:
  OLE2 BodyText 직접 파싱 → hwp5txt CLI → PrvText(미리보기) 스트림.
- **첨부 추출**: pdf(PyMuPDF, 텍스트가 빈약하면 OCR 폴백) / hwp·hwpx(hwp5 SAX) / docx(zip에서 document.xml) /
  pptx / xlsx(시트당 행 상한) / 이미지(OCR) / zip(1단계 재귀). 파일당 글자 수 상한을 둔다(예: 5만 자).
- **OCR 결과에는 경고 배너를 자동 삽입**: "⚠ 스캔 문서 OCR 결과 — 금액·숫자는 오독 가능, 정확한 수치는 원본 대조 필요."
- **규정집(있다면)**: 전 규정이 담긴 HWP를 조문(제N조) 단위로 verbatim 분해해 별도 소스 계열
  `extract/regulations/`로 둔다. 규정 제목 화이트리스트로 절 경계를 검증하고, 검증 리포트를 함께 생성한다.
- 멱등성: doc.md가 이미 있으면 건드리지 않는다(비파괴). 첨부 섹션은 기존 섹션 제거 후 재삽입(멱등).

**완료 기준**: 무작위 표본 150건에서 빈 템플릿·깨진 텍스트 없이 실내용이 추출됨
(원본 구현 실측 100% — 참고치이며 신규 구현의 필수 목표치는 아님).
실패 문서는 실패 목록 CSV로 추적된다.

**함정**
- **빈 템플릿 오인**: 본문 길이만으로 "있음" 판정하면 안 된다. 한글뷰어 호환 안내 배너 같은 상투문(마커 문자열
  목록으로 관리)이 본문으로 오인된다 — 원본 구현 실측에서 후보의 92%가 배너였다.
- **NFC/NFD 유니코드 정규화**: macOS 파일명과 문자열이 정규화 형태가 달라 비교·매칭이 조용히 실패한다.
  경로·파일명·CSV 대조는 반드시 NFC 정규화 헬퍼를 거친다.
- HWP 텍스트 추출이 만드는 고아 UTF-16 서로게이트는 `encode/decode(ignore)`로 제거하지 않으면 후속 단계가 죽는다.

### ③ 색인 (Indexing)

**목적**: extract 층 전체를 한 개의 SQLite DB로 색인해 즉답 검색을 만든다.

**스키마 계약**:
```sql
CREATE TABLE docs(id INTEGER PRIMARY KEY, doc_num, title, date, drafter,
                  template, kind, year, path, body_status);
CREATE VIRTUAL TABLE fts USING fts5(title, drafter, body,
                  content='', tokenize='trigram');
```
- `trigram` 토크나이저: 한국어 형태소 분석 없이 부분일치 검색이 된다(3글자 이상 질의).
  2글자 이하는 LIKE 폴백.
- `content=''`(contentless): 원문은 파일에 있으니 인덱스만 저장해 DB를 가볍게 유지.
- 소스 계열마다 색인 함수를 추가한다: 결재문서(`kind`=문서종류) / 규정(`kind`=규정·규정부칙·규정별표,
  합성 `doc_num="규정:{규정명} 제N조"`) / 공유자료(이사회·위원회 최종본, `kind`=공유자료).
  규정은 검색 재현율을 위해 fts body 앞에 규정명을 붙여 넣는다.
- 매일 밤 전체 재색인(수 분 이내면 증분 불필요 — 단순함이 이긴다).

**임베딩(선택, Tier1.5)**: BGE-m3-ko(1024차원)로 문단 경계 청킹(1,000자 목표) 후 sqlite-vec에 저장.
**단, 원본 구현의 자체 벤치마크에서 정형 문서 코퍼스는 FTS 단독과 하이브리드가 동률이었다.**
FTS 먼저 배포하고, 실제 질의 20~60개로 평가한 뒤 필요할 때만 추가하라.

**완료 기준**: CLI 검색 스크립트로 임의 키워드 10개를 검색해 기대 문서가 상위에 나옴.
색인 요약(계열별 건수)이 실제 파일 수와 일치.

### ④ 선별 위키화 (Selective Wiki-ization)

**목적**: 고가치 문서만 LLM으로 구조화 요약한다. **전량 위키화는 금지**(§2 사상 참조).

**구현 요점**
- 선별은 LLM이 아니라 **결정론적 SQL**로:
  - 제외: 반복 정형 문서 kind(근태신청서·지출결의서류 등 — 지식 가치 없음)
  - 포함: 제목에 고가치 키워드(이사회·위원회·예산·결산·규정·정관·지침·계획·협약·회의록 등 `[조직별 대체]`)
  - 민감문서 deny-list(⑤ 참조)는 선별 단계부터 배제(이중 방어)
- 배치 실행 위생 (원본 구현의 토큰 폭증 사고 교훈):
  - 429/사용량 한도 감지 시 **재시도 없이 즉시 전체 중단** (한도는 재시도로 안 풀린다)
  - 기술적 실패는 1회만 재시도 후 스킵, 상태 미갱신으로 다음 배치에서 자동 재후보화
  - 헤드리스 `claude -p` 자동화라면 세션 훅(핸드오프 요약 등)을 끄는 가드 환경변수 필수
    (원본 구현은 `CC_HANDOFF_GUARD=1` — 훅 스크립트가 이 변수를 보고 스스로 건너뛰는 자체 규약.
    각자의 훅 구성에 맞는 스위치를 만들 것), 세션당 컨텍스트 주입 고정비를 감안해 한 세션에 여러 건 처리
- wiki 폴더 구조: `concepts/ entities/ sources/ projects/(사업 허브) synthesis/(규정 허브 등) knowledge/(지식노트, ⑥)`
- 사업 허브: 사람이 쓴 개요 2~3줄 + search.db 제목 검색으로 연도별 관련 문서 목록 자동 삽입.

**완료 기준**: 선별 쿼리 결과가 전체 문서의 10~15% 이내. 위키 표본에서 환각(원문에 없는 내용) 0건.

### ⑤ MCP 서버 (연결)

**목적**: 검색·열람·지식저장을 MCP 도구로 노출해 ChatGPT/Claude가 직접 쓰게 한다.

**도구 계약** (FastMCP `@mcp.tool`):

| 도구 | 시그니처 요지 | 역할 |
|---|---|---|
| `search` | `(query, year, kind, limit, days, date_from, date_to)` | FTS 검색. **기간 파라미터만으로도 최신순 열거 가능**해야 함("어제 결재내역"류 질문은 검색 정교화가 아니라 열거형 입구가 필요). days와 date_from/to가 함께 오면 date_from/to 우선 |
| `read` | `(doc_ref, year)` | 문서 원문+결재선+첨부목록. 본문 미확보 문서는 raw에서 **읽기 전용 실시간 폴백 추출**(PDF→HWP(빈템플릿 스킵)→첨부, 글자 수 상한) |
| `attach_read` | `(doc_ref, file_name, max_chars)` | 첨부 1개의 실제 내용(HWP 표 포함). 파일명 없으면 목록 반환 |
| `knowledge_save` | `(topic, content_md, source_docs, sensitive)` | ⑥ 지식노트 저장 |
| `knowledge_list` / `knowledge_read` | | 노트 열람 |
| `pack_list` / `pack_map` / `pack_search` | | ⑦ 팩 질의 (팩 도입 시) |

**서버 instructions(도구 설명문)에 반드시 넣을 사용 규약**:
답변은 검색·열람 결과의 문서(번호·제목·날짜)를 인용할 것 / 결과에 없으면 "문서에서 확인 안 됨" /
답변 전에 지식노트 먼저 확인, 있으면 우선 근거로 / 재사용 가치 있는 결론은 knowledge_save로 저장.

**권한 분리 — 인스턴스 2개** (같은 코드, 환경변수로 분기):

| | 차단판 (일반 직원용) | 전체판 (관리부서용) |
|---|---|---|
| 포트 예 | 8770 | 8771 |
| 민감문서 | deny-list로 **색인 레벨에서 제외** (검색 결과에 아예 안 나옴) | 접근 가능 |
| 환경변수 예 | `ALLOW_SENSITIVE=0` | `ALLOW_SENSITIVE=1` |

- deny-list는 키워드 매칭이 아니라 **문서 단위 CSV**(문서번호+연도 키, 메타데이터 규칙으로 사전 생성).
  인사·급여·평가·채용·의료비 등 `[조직별 대체]`.
- 보조 방어로 키워드 판정을 겹친다: 민감 단어(급여·연봉 등) + 내부 맥락 단어(자사 직원 등) 동시 존재 시 차단,
  단 "협력기관 직원 급여" 같은 정당한 업무 문맥은 통과시키는 예외 규칙 포함.
- 규정류는 조직 공용 제도문서로 간주해 양쪽 모두 노출(개인 인사발령·급여명세 결재문서는 계속 차단).

**네트워크 노출 (외부 연결이 필요할 때만)**:
1. MCP 서버는 업무 PC의 **Tailscale 사설 IP에 바인딩** (공인 인터넷에 직접 노출 금지)
2. 공개 서버(Caddy)가 **추측 불가능한 시크릿 경로**(`/mcp-<랜덤hex>/mcp`)를 사설 IP:포트로 리버스프록시
3. ChatGPT: 유료 플랜 → 설정 → 커넥터(개발자 모드) → URL 등록, 인증 None — **URL 자체가 열쇠**이므로
   시크릿 경로를 문서·채팅에 평문으로 남기지 않는다. Claude: 유료 플랜 커스텀 커넥터로 동일 URL 등록.
4. **`stateless_http=True, json_response=True`** 필수 — ChatGPT 커넥터는 MCP 세션ID를 일관되게
   유지하지 못해 stateful 서버에 400("Missing session ID")을 낸다. 이 두 옵션이 그 해결책이다.
5. 커넥터는 도구 목록을 캐시한다 — 도구를 추가하면 커넥터를 껐다 켜야 보인다.

**운영**: LaunchAgent/systemd로 상시 구동(`KeepAlive`), 야간 자동 갱신 잡이 그래프·재시작·헬스체크
(JSON-RPC initialize 호출로 `serverInfo` 응답 확인)까지 수행.

**완료 기준**: 로컬 Claude Code에서 stdio로 도구 호출 성공 → HTTP 인스턴스 2개에서 민감문서가
차단판에서만 안 보임을 확인 → ChatGPT에서 실제 질문 1건이 문서 인용과 함께 답변됨.

### ⑥ 지식노트 (자가발전 루프)

**목적**: 질문→답변에서 나온 재사용 가치 있는 결론을 저장해, 같은 질문에 문서 재검색 없이
검증된 답을 먼저 내놓게 한다. 쓸수록 똑똑해지는 부분이다.

**노트 데이터 계약** (`wiki/knowledge/<주제-키워드>.md`):
```markdown
---
title: <주제>
type: knowledge-note
created: / updated: <날짜>
status: 자동저장 | 확인됨     # AI 저장은 항상 '자동저장', 사람 검토 후 '확인됨'
sensitive: 일반 | 민감        # 민감 노트는 차단판 인스턴스에서 존재 자체가 안 보임
sources: [<근거 문서번호>...]
---
## 질문
## 답변 요약
## 메모 / 갱신 이력
```

**저장 시 서버가 강제하는 검증** (`knowledge_save` 내부):
1. topic/content 빈 값 거부
2. `source_docs`(근거 문서번호) 필수 — **각 번호가 실제 볼트에 존재하는지 검증**, 없으면 목록과 함께 거부
3. 개인 인사정보 감지 시 저장 자체를 거부 (특정 개인의 급여·상여·평가·근태·인사는 저장 대상이 아님 —
   조직 공용 지식만)
4. **질문자(누가 물었는지)는 어떤 형태로도 기록하지 않음** (asker 필드를 의도적으로 두지 않는다)
5. 파일 크기 상한 (예: 20KB)
6. 같은 주제는 새 파일이 아니라 기존 노트 갱신 — 주제 슬러그를 정규화(NFC·공백 처리)한 뒤 완전일치로 판정
   (sources 병합, 갱신 시 status를 '자동저장'으로 되돌려 재검토 유도)

**루프의 나머지 반쪽**: `search` 도구가 검색 시 지식노트를 함께 검색해 별도 섹션으로 먼저 제시한다.
이게 없으면 저장은 되는데 재사용이 안 된다.

**주간 검증 배치(권장)**: 매주 1회 — ①(LLM 없이) 근거 문서번호 실존 재확인 ②(LLM 없이) 근거보다
최신 유사 문서 발견 시 "갱신 검토" 표시 ③(저가 LLM) 노트의 주장을 근거 원문과 대조해
PASS→`status: 확인됨`, FAIL→노트는 두고 사람 검토 목록에만 적재. 자동 삭제·수정 금지.

**완료 기준**: 실제 질문 1건의 결론이 저장되고, 같은 주제 재질문 시 검색 결과 최상단에 그 노트가 나옴.

### ⑦ 온톨로지 팩 (선택 — 주제 심화)

**목적**: "이 주제만큼은 깊고 정확하게" 답해야 할 때, 주제 하나를 위한 지식 상자를 만든다.
전사 검색(③)이 도서관 전체라면, 팩은 시험 범위만 정리한 요약 노트다.

**팩 파일 구성**:
```
packs/{팩이름}/
  ├ mission.md        # 수집 범위·승격 규칙·합격 기준(시험 문항) — 시작 전에 동결
  ├ 지도.md            # 사람용 전체 지형 (맨 앞에 "질문→답 위치" 표)
  ├ nodes.jsonl        # 개념 노드 (모든 항목에 evidence_refs 필수)
  ├ edges.jsonl        # 관계 ("관련있다" 금지 — 의미 있는 관계타입만)
  ├ evidence/*.jsonl   # 근거 발췌 (원문 excerpt 강제, EV-A/B/C ID 체계)
  ├ 시험-답안.md        # 블라인드 시험 결과
  └ quality-report.md  # 게이트 검사 결과
```

**5단계 파이프라인** (OpenCrab 오픈소스에서 차용, 2회 연속 파일럿 합격으로 검증됨):
1. **미션 동결** — 수집 범위·합격 기준(시험 문항)을 먼저 고정. 이게 팩의 지능 상한을 정한다
2. **수집** — 소스별 병렬 수집, 모든 증거에 원문 발췌 강제
3. **추출** — 노드/엣지 생성, 증거 없는 항목은 draft 격리
4. **게이트 검사** — 독립 검사자가 참조 무결성 전수 + 원본 대조 표본. 위조 1건이면 FAIL
5. **블라인드 시험** — 팩 파일만 보고 합격 기준 질문에 답하게 해 채점

**핵심 교훈**: **팩은 동결된 질문만큼만 똑똑하다.** 실사용에서 못 답하는 질문이 나오면
증분 패치(append-only: 증거·노드 추가)+문항 재설계로 6단계째 루프를 돈다. 문항은 시스템 주인이 직접 낸다.

---

## 4. 운영 자동화 (야간 파이프라인)

매일 밤 1회, 한 스크립트가 순서대로 (원본 구현: LaunchAgent, 22:30):

```
a)  증분 수집 (①)                       — 실패해도 다음 단계 진행
b)  신규 문서 텍스트 추출 (②)
b2) 본문 refill 큐 처리 (②, 일일 상한)    — 실패해도 다음 단계 진행
c)  전체 재색인 (③)                      — 실패 시 파이프라인 실패
d)  임베딩 증분 (③에서 도입한 경우에만 이 단계 포함) — 도입했다면 실패 시 파이프라인 실패
    완료/실패를 메신저(디스코드 등)로 보고
```

**침묵 실패 가드(필수)**: "대기열이 있는데 실행 후 줄지 않으면, 리턴코드가 0이어도 실패로 간주하고 경보."
원본 구현에서 ERP 뷰어 UI가 개편되자 refill이 이틀간 조용히 헛돌았던 사고의 재발 방지책이다.
외부 시스템(ERP 화면)에 의존하는 자동화는 언젠가 반드시 조용히 깨진다 — 진전량을 감시하라.

---

## 5. 마일스톤 (이 순서로 만들 것)

| | 마일스톤 | 완료 기준 |
|---|---|---|
| M1 | 수집 + 추출 표본 | 표본 20건 raw 폴더 생성, 150건 추출 표본 정상 |
| M2 | FTS 색인 + CLI 검색 | 키워드 10개 검색 적중 |
| M3 | MCP 로컬 연결 (Claude Code stdio) | 도구 호출로 실문서 검색·열람 |
| M4 | 권한분리 + 원격 노출 + ChatGPT 커넥터 | 민감문서 차단 확인, ChatGPT에서 인용 답변 |
| M5 | 지식노트 루프 | 저장→재질문 시 우선 제시 |
| M6 | (선택) 팩 1호 | 게이트 PASS + 블라인드 시험 통과 |

M1~M2까지는 LLM API 비용이 사실상 0이다. M3부터 실사용 검증을 하며 넓혀 간다.

---

## 6. 안전 불변 조건 (어떤 단계에서도 우회 금지)

1. **ERP는 읽기 전용**: 조회·로그인·PC저장(다운로드)만. 상신·결재·승인·삭제·입력 버튼은 절대 클릭하지 않는다.
2. **raw는 비파괴**: 수집 원본은 수정·삭제하지 않는다. 고칠 게 있으면 파이프라인을 고쳐 재생성한다.
3. **개인정보 4중 방어**: 문서단위 deny-list(색인 레벨) + 키워드 맥락 판정 + 지식노트 개인정보 저장 거부 + 질문자 비기록.
4. **환각 억제**: 인용 강제, 결과에 없으면 "확인 안 됨", OCR 수치는 경고 배너.
5. **비용 위생**: 429 즉시 전체 중단, 전량 LLM 처리 금지, 헤드리스 배치는 훅 가드.
6. **삭제는 사람만**: 자동화가 파일·레코드를 지우는 로직을 넣지 않는다.
7. **raw와 민감 데이터는 git·클라우드 동기화에 올리지 않는다** (.gitignore, 로컬+백업만).

---

## 7. 함정 전체 목록 (원본 구현 실측)

1. NFC/NFD 유니코드 정규화 혼재 — 문자열 비교 전 반드시 NFC 정규화
2. body.hwp가 빈 양식 템플릿 — 마커 없으면 신뢰 금지
3. 한글 COM은 SSH 비대화형에서 무한 정지 — 헤드리스는 hwp5
4. 429 한도는 재시도로 안 풀림 — 즉시 전체 중단
5. ERP 로그인 확인 팝업의 "확인" 버튼이 실제 업무 액션(출근 체크)일 수 있음
6. 뷰어 호환 안내 배너가 본문으로 오인됨 (실측 오탐 92%)
7. 문서번호 연도별 재사용 — (번호+연도) 키
8. 첨부 API 파라미터 누락 시 전체 zip 반환
9. 삭제 문서 마커 없으면 무한 재수집
10. refill은 비파괴(새 파일+새 마커) — 기존 마커 재사용 시 빈 템플릿 오인 사고
11. OCR 재무 수치 오독 — 경고 배너 + 원본 대조 원칙
12. UI 개편에 의한 침묵 실패 — 진전량 가드
13. HWP 서로게이트 문자 — encode/decode(ignore) 정리
14. 커넥터의 도구 목록 캐시 — 도구 추가 후 커넥터 재등록
15. stateful HTTP MCP는 ChatGPT에서 400 — stateless_http + json_response

---

## 8. 부록 A — 원본 구현 파일 지도 (HC 환경)

| 역할 | 위치 |
|---|---|
| 볼트(데이터) | `~/AmaranthVault/` — raw/ extract/ index/ wiki/ accounting/ docs/ |
| 수집 파이프라인 | `~/Projects/amaranth-sync/` — collect_approval.py, daily_update.py, daily_pipeline.sh, amaranth/signer.py(SignedClient), budget/ |
| 추출·색인 스크립트 | `~/AmaranthVault/scripts/` — extract_daily_mac.py, refill_bodies_mac.py, extract_attachments_mac.py, ingest_regulations.py, build_search_index.py, embed_batch.py, erp_login.py |
| 선별 위키화 | `~/AmaranthVault/scripts/` — wiki_select.py, wiki_run.py, curator_invoke.py, build_project_hubs.py, build_regulation_hubs.py |
| 지식노트 검증 | `~/AmaranthVault/scripts/knowledge_verify.py` (주 1회) |
| MCP 서버 | `~/ontology-lab/scripts/mcp_server.py` (단일 코드, 환경변수로 인스턴스 분기) |
| 온톨로지 팩 | `~/ontology-lab/packs/` (claude-code-edu, claude-design-work + SKILL-DESIGN-NOTES.md) |
| 배포 스크립트 | `~/ontology-lab/deploy-amaranth-mcp.sh` |
| 자동화 스케줄 | `~/Library/LaunchAgents/com.hc.amaranth-daily`(22:30 수집), `com.hc.amaranth-mcp-refresh`(23:20 MCP 갱신), `com.hc.ontology-mcp-amaranth{,-support}`(상시), `com.hc.amaranth-knowledge-verify`(일 21:00) |

## 부록 B — 용어 최소 사전

- **MCP(Model Context Protocol)**: AI(ChatGPT·Claude)에 외부 도구를 꽂는 표준 규격. 이걸로 AI가 우리 검색 DB를 직접 조회한다.
- **FTS5 / trigram**: SQLite 내장 전문검색. trigram은 글자를 3자씩 잘라 색인해 한국어 부분일치가 되게 하는 방식.
- **CDP(Chrome DevTools Protocol)**: 크롬을 코드로 조종하는 통로. 이미 로그인된 창을 재사용할 수 있다.
- **deny-list**: 보여주면 안 되는 문서의 명단. 검색 색인 단계에서 아예 빼 버리는 게 핵심.
- **온톨로지**: 개념과 개념 사이의 관계까지 정리한 지식 지도. 문서 더미와 달리 "무엇이 무엇과 어떻게 연결되는지"를 안다.
- **RAG**: AI가 답하기 전에 관련 문서를 찾아 읽고 근거로 삼게 하는 방법.

---

*작성: 2026-08-06 · 원본 구현 기간: 2026-06-07 ~ 07-29 · 이 설계서는 실코드 조사와 당시 대화 기록(ChatVault)을 원자료로 작성됨*
