<!-- canonical: https://docs.axelabs.ai/ops/decisions -->
<!-- source: content/ops/decisions.mdx -->

---
title: 아키텍처 결정 (DECISIONS)
description: D1~D-ops-19 누적 결정 기록 + 함정 모음.
---

# 아키텍처 결정 누적 기록

다음 두 SSOT 파일에 결정이 누적되어 있고, 본 페이지는 그 요약입니다. 변경 발생 시 그쪽 파일이 우선이고 본 페이지는 후행 정리:

- [/Users/axe/multi-tenant-platform-plan.md](https://github.com/soohunkang/blueprint/blob/main/multi-tenant-platform-plan.md) — 마스터 플랫폼 plan + D 결정 + 함정
- [/Users/axe/frame/DECISIONS.md](https://github.com/soohunkang/frame/blob/main/DECISIONS.md) — frame 측 D-ops-1 ~ 15

## D — 플랫폼 결정

| ID | 결정 | 채택일 |
|---|---|---|
| D1 | **격리 = customer 1개 / macmini 1대** (OS-level, SPOF 수용) | 2026-05 |
| D2 | **테넌트 ID = 이메일 도메인** (customers.yaml SSOT) | 2026-05 |
| D3 | **path-based 1-level subdomain** (`{customer}.axelabs.ai/{service}`) | 2026-05 |
| D4 | **Tailscale 메시 + SSH key** (자체 mTLS 안 만듦) | 2026-05 |
| D5 | **HTTP MCP + 부트스트랩 JWT** (OAuth 1차 deferral) | 2026-05 |
| D6 | **push-based CI/CD** (pull polling 안 함) | 2026-05 |
| D7 | **P2P ring restic backup** (mesh 안 만듦) | 2026-05 |
| D8 | **3-tier secret** (vault + keychain + env) | 2026-05 |
| D9 | **schema-per-entity** (고객 내 법인 격리, RLS 대신) | 2026-04 |
| D10 | **index / vault 동일 패턴** (Phase 6 deferral) | 2026-05 |
| D-config-13 | **다운타임 0 frame deploy** (blue/green + host proxy, cloudflared 미관) | 2026-05-15 |
| D-config-14 | **DR cold storage** (external SSD rotation, 종이 password) | 2026-05-15 |
| D-config-15 | **blueprint multi-customer** (env override per macmini) | 2026-05 |
| D-config-16 | **blueprint = Postgres** (SQLite deprecate 예정) | 2026-05 |
| D-config-17 | **Postgres cutover 무중단** (운영자 명시 승인) | 2026-05 |

## D-ops — frame 운영 결정

| ID | 결정 | 채택일 |
|---|---|---|
| D-gate-5 | **e-sign 직접구현 = 암호등급 자체 서명엔진 ([D-gate-3](/ops/decisions) 모두싸인 integrator 입장 supersede)** (2026-06-07 — 빌드+검증 완료, 운영자 pivot) — [D-gate-3](/ops/decisions) 은 e-sign 코어를 **모두싸인이 흡수**(integrator)하고 직접-build 서명엔진을 Phase-2 로 defer 했으나, 운영자가 *"모두싸인의 기능을 직접, 암호등급으로 구현"* 지시 → gate Phase-1 = **gate-native 직접-build 암호 서명엔진**(작동 self-hosted, 이 세션 빌드+검증). **무엇이 빌트됐나**: ① **CMS/PKCS#7 CAdES-BES** detached 서명(RSA-2048+SHA-256), `DocumentSigner` trait(**HSM-ready**) + `SoftwareSigner`(self-signed X.509) — 적대적 리뷰(위조 도달경로 없음) + openssl 양방향 호환, 7 tests. ② **RFC3161 신뢰 타임스탬프 → CAdES-T**(hand-rolled ASN.1, 단일 clean dep; default DigiCert RSA TSA, 한국 prod = Koscom/CrossCert config-swap), 9 tests + live round-trip. ⑤ **워크플로 배선**: 시행(execute) 시 `esign.gate` actuator 가 승인된 결정을 **server-seal**(canonical decision bytes → CAdES-T), append-only **`esign_seal`** evidence 테이블(전자문서법 §5 custodian) 저장. 신규 **`verify_seal`** MCP tool(서명+무결성+타임스탬프 검증). e2e: 결재→seal→verify valid, 저장 CMS openssl cross-verify 통과, DB tamper→integrity fail. **법적 근거(전자서명법 2020, 공인폐지)**: 사서명(private/internal e-sign)은 **실지명의 + 서명의사**면 유효 — gate 의 경우 **실지명의 = 인증된 gate identity**(내부 서명자), **서명의사 = 결재 승인 행위** → 운영 라이선스 불요·합법. **유지된 경계(여전히 Phase-2/하드닝)**: 외부 서명자 **본인확인 = 방통위 지정 본인확인기관** 통합(정보통신망법 §23-3, 자가구축 불가) = **진짜 Phase-2** · HSM(cryptoki/PKCS#11, abstraction ready) · PAdES 서명-PDF 컨테이너(③④ typst deterministic render + pyHanko) · production 키관리(dev = ephemeral key) = **하드닝**. **red-team P0 해소-또는-명시**: 직접-build 를 막았던 P0(HSM·HTML→PDF 결정성·custody)가 — 결정성은 **canonical bytes 로 now**(typst PDF 는 later), HSM 은 **abstraction ready**(trait), custody 는 **esign_seal + RFC3161 TSA** 로 해소-또는-명시. **MUST NOT(불변)**: 자체 CA·per-user cert·자체 TSA·"공인/인증" 주장 — gate 는 **단일 서버 서명 identity**(ModuSign-recipe 모델: per-user cert 없이 서버가 봉인). **스택**: RustCrypto `cms`/`der`/`rsa` + RFC3161 + DigiCert TSA. 갱신: [/architecture/governance](/architecture/governance) §6 재작성 + [para-os §84](/architecture/para-os) footnote + [D-gate-1](/ops/decisions) Phase-1 e-sign 행. 정합: [D-gate-1](/ops/decisions)(scope) · supersede [D-gate-3](/ops/decisions)(e-sign 경계). | 2026-06-07 |
| D-gate-1 | **gate = 한국형 결재+e-sign+법무 거버넌스 제품 — narrowed Phase-1 확정(경로 A), 야심 5종 Phase-2 defer** (2026-06-06 결정 → 코드 착수 직전; 마스터플랜 [B-bp-decision-pipeline-esign](/ops/backlog), 상세 [/architecture/governance](/architecture/governance)) — gate = 신규 **Rust+Postgres** 서비스(포트 41xx: pg4100/blue4110/green4111/proxy4112, origin `axe.axelabs.ai/gate`), 한국형 **내부 전자결재 + e-sign + HTML 문서 + 법무검색**을 AI-driven 결정→실행 파이프라인으로. **동기**: 7라운드 설계 후 7-agent red-team 이 풀설계(결재+직접 e-sign+법무 RAG flywheel+공개 SaaS+결제+멀티테넌트 = 1인이 동시에 짓는 6~7 규제제품)에 **NO-GO**(11 P0 / 16 P1 / 8 누락) 판정 — 11 P0 가 **전부 야심 5종에 거주**. → **경로 A(narrowed Phase-1)** 채택: 11 P0 를 *해소가 아니라 defer 로* 회피. **Phase-1(확정·착수 가능)**: ①결정로그(record≠process, [D-gate-2](/ops/decisions)) ②결재워크플로 상태기계(기안→DOA→승인/반려/보류→시행) ③document = HTML spine→PDF 렌더 ④**서명 signature** — *2026-06-07 [D-gate-5](/ops/decisions) 로 갱신*: 모두싸인 통합(provider=modusign, [D-gate-3](/ops/decisions))에서 **gate-native 직접-build 암호 서명엔진(CAdES-T server-seal + esign_seal + verify_seal)** 으로 전환, 모두싸인 = optional alternative provider 로 강등 ⑤actuation ledger(멱등+reconcile) ⑥Blueprint `gate.decision` citation mirror. **AXE-internal only**, **pgvector-KR**(Pinecone 아님), 법무=**내부검색만**(결재 자동주입 X, [D-gate-4](/ops/decisions)). 결제·멀티테넌트 **없음**. **Phase-2+(각자 규제/substrate 게이트, defer)**: 외부 서명자 **본인확인기관** 통합(정보통신망법 §23-3, [B-gate-bonin-id-contract](/ops/backlog)) — *직접 e-sign 엔진 자체는 [D-gate-5](/ops/decisions) 로 Phase-1 빌트; 잔여 하드닝 = HSM·PAdES-PDF·prod-key* · Pinecone 법무 flywheel(+PIPA) · 공개 SaaS(+tenant_id RLS substrate·self-IdP) · 결제(포트원/토스/Paddle, +전자금융/전자상거래) · cross-tenant 축적(+PIPA 가명정보). 빌드게이트 = [B-gate-legal-counsel](/ops/backlog) + [B-gate-phase2-gates](/ops/backlog). **착수 순서**: docs-first(D-gate-1~5 + governance.mdx) → `cp -R cortex gate` scaffold(auth 3-branch ladder·RLS·blue/green 상속) → 7테이블 migration(+`esign_seal`) → 상태기계 → **`esign.gate` actuator**(gate-native CAdES-T server-seal + verify_seal, [D-gate-5](/ops/decisions) 빌트) + 모두싸인 alt-provider stub(optional). 기준 = [para-os §7](/architecture/para-os) refine, [D-bp-para-1](/ops/decisions) governance clause 확장. | 2026-06-06 |
| D-gate-2 | **결정 record SoR = gate `decision` 테이블, Blueprint = `gate.decision` citation mirror — D-bp-para-1 governance clause refine** (2026-06-06) — [D-bp-para-1](/ops/decisions)/[para-os §7](/architecture/para-os) 은 결정로그를 *"Blueprint core primitive(가로)"* 로 뒀으나 = AXE-internal 전제. **standalone 외부제품 요구 + 3축 survey(SoR경계·auth·멀티테넌트)** 가 refine: **record SoR = gate** `decision` 테이블(완료 workflow + 서명 HTML문서 + cert·authority·actor·decided_at·basis citations·supersedes, append-only). **Blueprint = mirror**: citation kind `gate.decision`(artifact·Area 가 cite) + 후일 visibility-gated mirror 테이블 — optional consumer 1종, 외부고객 = OFF(gate 단독 완결). **근거(grounded)**: [/architecture/artifacts](/architecture/artifacts) "MCP backend = sole owner, data 중복 0" · [/architecture/data](/architecture/data) cross-service = 직접 DB read 금지·명시적 신호채널(pg_notify/outbox)만 · **[D-cortex-9](/ops/decisions) = service-owns-SoR + Blueprint visibility-gated mirror 이미 빌트**(template 기성) · [para-os §7](/architecture/para-os) e-sign cert 를 citation 으로 = pre-authorize. → round-1 의 "record→Blueprint core" 가 오히려 platform 패턴(service-owns-SoR) 이탈, standalone 요구가 아키텍처가 가리키던 곳을 드러냄. 상세 = [/architecture/governance](/architecture/governance). | 2026-06-06 |
| D-gate-3 | **actuation ledger(결정→실행 멱등 일관성) + e-sign = 모두싸인 integrator(gate = 전자문서법 §5 evidence custodian); 직접-build 서명엔진 = Phase-2 gate** (2026-06-06) — ⚠️ **e-sign 부분은 [D-gate-5](/ops/decisions)(2026-06-07) 가 supersede** — 직접-build 암호 서명엔진이 Phase-1 으로 당겨져 빌트됨(아래 e-sign 경계는 역사 기록). actuation ledger 부분은 유효. 승인된 결정 → actuator 호출(`frame.post_journal`/`index.register_deal` 등, [para-os §7](/architecture/para-os) "Area 서비스 = actuator"). **actuation 테이블** = 실행 ledger(actuator_kind·payload·**idempotency_key=uuidv5(workflow+seq+actuator+payload_hash)**·state pending/inflight/done/failed·result_ref·reconciled_at) + **reconcile 워커**(frame-worker 패턴, pg_notify). **e-sign 경계(Phase-1 = INTEGRATE, [para-os §84](/architecture/para-os) 와 정합)**: signature.provider=`modusign` — 본인확인·타임스탬프·증거능력 코어를 모두싸인이 흡수, gate 는 **전자문서법 §5 evidence custodian**(서명문서+메타[작성자·수신자·송수신/서명 일시] 무결성 보관 + consent 기록, immutable audit store). cert ID 만 citation 참조(복제0). **⚠️ 직접-build 서명엔진(자체 PKI 서버서명·PAdES/XAdES·HTML→PDF 결정성[sealed-bytes=원본]·해시봉인·공개검증포털·감사추적인증서·HSM/KMS·서명키 DR/escrow·대법원 2017도13263 custody·본인확인기관 직통합) = Phase-2 gate**(red-team P0 #3/#7/#10) — 정당화였던 KISA §8 인정조차 "MVP 불요". **MUST NOT(Phase 무관)**: 자체 CA·per-user cert·자체 TSA·"공인/인증" 주장. **멱등 보강**: 모두싸인 actuator 도 provider-side idempotency 토큰 필요(gate idempotency_key 가 외부 이중실행 못막음, red-team P0 #5); saga 보상(e-sign+결제+분개) = 결제 도입 Phase-2. 상세 = [/architecture/governance](/architecture/governance). | 2026-06-06 |
| D-gate-4 | **법무 모듈 가드레일 = 변호사법 §109/§34 검색·정보 한정(자문 금지); Phase-1 = pgvector-KR 내부검색만, flywheel = Phase-2** (2026-06-06) — gate 법무모듈 = **검색·요약·번역·템플릿(로폼: user-blank, 기계적)만**. **하드룰 변호사법 §109(형사)·§34(5)**: 법률상담/사안별 자문/대리/결과예측-as-advice **금지**, **공개 "AI 법률상담" 금지**(AI대륙아주 7개월 폐쇄·징계 선례), 변호사 노출 시 정액광고만(로톡 모델, 알선료/성공보수/광고수익배분 금지). 모든 출력 "**법률 정보, 자문 아님**" disclaimer + cite = **retrieval-verified**(존재 + 시행일/선고일 temporal 강제). **결재 자동주입 차단**(AI 기안이 법령/판례를 auto-approve 경로에 주입 X — red-team P0 #2). **Phase-1 data**: 국가법령정보 OPEN API(open.law.go.kr, 이용제한없음·무료) + data.go.kr 판례(상용 OK) + HF `korean_law_open_data_precedents`(openrail), 대법원/헌재+법령 grade(하급심=상용 walled 제외). SoR = gate Postgres, index = **pgvector-KR**(Pinecone 아님). **Phase-2 gate**: Pinecone+Cohere rerank+Gemini embed flywheel(LLM비용 사용자부과·DB 복리) + cross-tenant 축적 = **PIPA 처리근거·가명정보·국외이전 자문**([B-gate-legal-counsel](/ops/backlog)) 선결 — `lbox_open`/`kcl`=CC BY-NC=유료제품 금지. B2B/internal-use 유지 = 최저위험. 상세 = [/architecture/governance](/architecture/governance). | 2026-06-06 |
| D-ops-44 | **Vault 비밀 주입 = SSH 환경 raw-bw keychain-free (`axe vault unlock` 금지)** (2026-06-09, `/Users/axe/CLAUDE.md` 강제 규칙으로 박제) — 이 머신 = AXE Mac mini, Claude Code 세션은 **SSH(loopback) 위에서** 돌고 운영자는 Windows 에서 `ssh axe@100.127.210.30` (Tailscale) 로 접속. macOS 는 **SSH 세션이 GUI login keychain 에 쓰는 것을 차단**("User interaction is not allowed") → vault 비밀을 *넣는* 경로의 keychain 의존이 전부 막힘. **금지(SSH 에서 전부 실패)**: (1) 운영자에게 `axe vault unlock` 시키기 — keychain-cache 단계가 SSH 에서 crash. (2) `axe secret push/pull/check` 로 비밀 PUT — `_vault_env` 가 keychain 세션만 읽음(`security find-generic-password -s axe.vault.session -a ai@axellc.com`) → SSH 에서 "vault session not found". (3) osascript `display dialog` — GUI 다이얼로그라 원격/Windows 운영자에게 안 보임(운영자-타이핑 비밀은 `read -rs`). **정답 = keychain-free raw-bw**, 운영자가 자기 인터랙티브 셸(Windows `ssh axe@…` 또는 Mac 터미널)에서 직접 실행. 에이전트는 **ASCII-only·주석 없음·`bw unlock` 을 단독 한 줄**로 공급(한 블록 붙여넣기는 unlock 프롬프트가 다음 줄을 비밀번호로 삼켜버림). 단계: ① `export NODE_EXTRA_CA_CERTS=/Users/axe/.axe/vault/certs/rootCA.pem`(Vaultwarden private CA — 필수) ② **단독 줄** `export BW_SESSION="$(bw unlock --raw)"`(master-pw 프롬프트; TTY-flaky 폴백 `read -rs PW; export BW_SESSION="$(BW_PASSWORD="$PW" bw unlock --passwordenv BW_PASSWORD --raw)"; unset PW`) ③ raw `bw create`/`bw edit` 로 Login item — **name** = `customers.yaml` 의 `services.<svc>.secrets[].vault` 경로(예 `gate/axe/jwt-secret`), **username** = env var 이름, **password** = 스크립트 안에서 생성/읽은 값(`$(openssl rand -hex 32)`/`$(cat key.pem)`/상수 — echo·history·argv·agent-context 어디에도 안 남김) → `bw sync` → `unset BW_SESSION`. **검증**: 에이전트는 검증 불가(세션이 운영자 셸에만 존재) → 운영자의 `created/updated` + `synced` 출력이 곧 확인, `axe secret check` 호출 금지(keychain 필요). **keychain 세션이 필요한 곳 = deploy/ship 시점 한정**(`axe ship`/`secret pull` 이 그때 keychain 읽음): SSH 에서 `read -rs KCPW; security unlock-keychain -p "$KCPW" ~/Library/Keychains/login.keychain-db; unset KCPW`(headless unlock, GUI 없음) → `axe vault unlock` → `axe ship`. 비밀을 *넣는* 작업엔 불요. 정합: [D-ops-17](/ops/decisions)(vault SoT) · [D-ops-40](/ops/decisions)(osascript hidden-dialog — GUI 세션 전제라 SSH 에선 본 raw-bw 가 대체). | 2026-06-09 |
| D-ops-43 | **운영자 원격 데스크톱 = RustDesk(Direct IP) over Cloudflare Access — Google Remote Desktop 대체, Tailscale 비의존** (2026-06-05 결정 → Phase 1 구축 중) — GRD(Google relay 경유 + Google 의존) 를 자체 트러스트 경계로 대체. **전송**: RustDesk 를 Direct IP 모드(자체 relay/ID 서버 미사용)로 axe-macmini 에서 구동 + `axelabs-tunnel`(d8efecdd) 에 TCP ingress (`rdp-axe.axelabs.ai → tcp://host.docker.internal:<port>`) 추가 → **`ssh-axe` ([D-ops-39](/ops/decisions)) 와 동일 Cloudflare Access 패턴**. 데스크톱 = `cloudflared access tcp --hostname rdp-axe.axelabs.ai --url localhost:<port>` 후 RustDesk 클라이언트 127.0.0.1 직결; 모바일 = Cloudflare WARP 등록 (`cloudflared access` CLI 가 폰에 부재). **인증 (2-phase)**: Phase 1 = **Microsoft Entra 직결 IdP** (즉시 가동, Blueprint 코드변경 0; 복구 채널이라 플랫폼 비의존이 오히려 견고 — 순환 의존 회피). Phase 2 fast-follow = **Blueprint OIDC 主 + Entra break-glass 副** ([B-blueprint-oidc-third-party-rp](/ops/backlog)) — 현 Blueprint OIDC (`src/lib/platform-oidc.ts`) 가 public·PKCE-only·exact-match 콜백 화이트리스트라 제3자 confidential RP (Cloudflare Access) 를 못 받음 → 콜백 허용 + client_secret 처리 + id_token/userinfo 완결성 확장 필요 ([D-axe-idp-1](/ops/decisions) Phase 2 영역). **Tailscale 배제 근거**: 인터랙티브 운영자 접속의 트러스트·전송은 Cloudflare Access 단일화 (ssh-axe 동형). Tailscale ([D4](/ops/decisions)) 은 backend (배포 push + 백업 ring) 전용 — RustDesk+Tailscale 안 함. **비용 $0** (Cloudflare Zero Trust Free ≤50 user; Spectrum 유료 경로 미사용). **macOS 26 Tahoe 함정**: RustDesk 에 화면 녹화 + 손쉬운 사용 권한 1회 수동 부여 필수 (SIP 보호, CLI 불가) + 주기적 재확인 nag. **거부된 대안**: (a) RustDesk+Tailscale — Tailscale 비의존 방향 역행. (b) RustDesk public relay / 자체 hbbs·hbbr — privacy + 추가 인프라. (c) Cloudflare Spectrum — 유료. (d) Blueprint 主 from start — 플랫폼 OIDC 코어(5 서비스 의존) 선확장 부담 → working RD 지연. (e) macOS 화면 공유(VNC) — Apple 코덱 성능 열위 (RustDesk 코덱 우선, 권한 마찰 수용). 본문: [/architecture/topology](/architecture/topology) + [/ops/inventory](/ops/inventory). **정정 (2026-06-05 동일 결정 진화): `{customer}.axelabs.ai/vnc` 경로 요구 → RustDesk(raw TCP) 폐기, 브라우저 VNC(Apache Guacamole) 전환.** path 라우팅 = HTTP 전용이라 TCP 불가 (ssh-axe flat 과 동일 D-ops-39 제약); Cloudflare 네이티브 browser-render VNC 도 hostname-bound (path 미지원, 2026-06 docs 확인) → 진짜 HTTP 앱 **Guacamole** 를 `/frame` 처럼 path 로. origin = macOS Screen Sharing(:5900) ← guacd ← guacamole webapp ← tunnel `http://host.docker.internal:<port>` ← Access(path-scoped, Entra). 이득 = 브라우저 클라이언트리스(모바일 ✓ — WARP 갭 해소) + 커스터머별 표준 path 템플릿. 트레이드오프 = RustDesk 코덱 → Apple VNC 코덱(관리/제어 충분). 초기 RustDesk-flat(`rdp-axe`, Access app `0653d6ef`) 자원 철거(2026-06-05). Phase 2 Blueprint SSO 는 Guacamole(HTTP)가 `Cf-Access-Authenticated-User-Email` 헤더 passthrough 로 RustDesk 보다 용이. **★ 최종 결론 (2026-06-05, 실측 후): GRD(Google Remote Desktop) 존치, 자가호스팅 대체 보류.** noVNC `/vnc` 를 e2e 구축·검증했으나 **Apple 레거시 VNC 코덱이라 체감 사용 불가** (Retina 프레임버퍼 + RFB + CF 다단 홉). RustDesk = 코덱 우수하나 네이티브 앱(브라우저/path 불가). **GRD 우위 = WebRTC/VP8 비디오 코덱을 *브라우저 안에서* + 네트워킹 셋업 0 + 모바일 — 이 조합을 자가호스팅으로 매칭하는 솔루션이 현재 없음** (Guacamole/noVNC/Cloudflare browser-render 전부 `:5900` 에 RFB 라 코덱 천장 동일; Mac 은 RDP 미지원이라 Guacamole RDP 경로도 불가). `/vnc` 실험 자원(noVNC 스택 `/Users/axe/axelabs/vnc/` + Access app `eae049e7` + ingress path rule + vault VNC pw) 철거 대상. **de-Google 미래 경로** = (a) RustDesk web client 성숙 시 self-host (코덱+브라우저 양립), (b) WebRTC 기반 자체 클라이언트(tether). 그 전까지 운영자 원격 데스크톱 = **GRD** (project_devenv "GRD 유지" 와 정합 — 본 결정이 실측으로 재확인). **수용한 trade-off**: GRD = 폐쇄소스(Google 독점 Chrome Remote Desktop — 호스트 코드 일부만 Chromium `src/remoting/` 공개, 릴레이·시그널링·디렉터리는 Google 비공개·self-host 불가) + 트래픽 Google relay 경유 = sovereignty 비용을 de-Google 솔루션 부재 시점까지 **의식적으로 수용**. | 2026-06-05 |
| D-ops-42 | **배포 SSOT 아키텍처 — `origin/main` SHA = 배포 진실원천** (worktree 작업격리 + build-from-SHA + deploy lock + 스테이지 상태기계 + migration-validation 게이트) (2026-06-04, DESIGN LOCKED · additive rollout) — **원칙**: `origin/main 의 commit SHA = 배포의 SSOT`. working tree·로컬 main·실행 중 컨테이너는 전부 그 SHA 의 파생·일회용 투영. docs·backlog 가 이미 matrix-postgres SSOT 규율([D-matrix-3](/ops/decisions))을 따르듯, 같은 규율을 **배포**로 확장. **동기 (실측 incident)**: N개 동시 Claude Code 세션이 repo 당 1 working tree + 1 `main` 공유 → (a) 다른 세션이 먼저 push 하면 `main` diverge → non-fast-forward push 거부, (b) 서로 다른 feature 의 uncommitted WIP 가 한 tree 에 누적 → 누구도 자기 slice 만 clean commit 불가, (c) `axe deploy` 가 dirty working tree 에서 이미지 빌드 → uncommitted 코드 prod 누출 + push 전 배포 가능. 2026-06-04: 이미 배포된 blueprint fix (commit `b4067504`) 가 main diverge + 타 세션 WIP 로 수 시간 push 불가 → 운영자 수동 reconcile (stash→rebase→push). 추가 발견: 운영자 `axe` CLI (364KB) 자체가 **버전 미관리·in-place 편집** = 같은 병의 가장 깊은 사례. **컴포넌트 A–F**: (A) **작업 격리** `axe work <svc> <slug>` = 세션별 git worktree (`~/.worktrees/<svc>/<slug>`) off origin/main, 정규 repo 는 fast-forward 전용 "main mirror" 강등(손편집 금지) → 공유 tree 경합·WIP 혼재·stash 더미 **발생 불가능**. (B) **SHA 에서만 빌드** = deploy 가 working tree 아닌 pushed SHA 의 clean checkout(`git worktree --detach`/archive)에서 빌드, 이미지 태그 `<svc>:<sha>`, push 안 된 SHA 배포 거부 → deploy-before-push + dirty-tree 누출 **구조적 불가능**. (C) **Deploy lock** = 서비스당 배포 직렬화 (matrix-postgres `pg_advisory_lock` 우선, 파일락 fallback) → 두 세션 동시 blue/green flip 불가. (D) **Provenance + drift** = 컨테이너 `org.axe.git_sha` 라벨, `axe health`/`axe host` 가 color 별 실행 SHA + origin/main 대비 drift 표시 → "라이브가 git 과 일치하나" 항상 답. (E) **잘못된 경로 제거** = `axe deploy` 가 tree 입력 폐기, `git push origin main` 직접 = pre-push 훅 + GitHub branch protection 으로 **기계적 차단**(현 honor-system 대체), `axe ship` 만 main 전진. (F) **통합 ship** = `axe ship`(worktree 에서): fetch → origin/main rebase → SHA build+test → push(main ff) → 그 SHA deploy(lock 하) → mirror ff → shiplog. 오늘 손 reconcile 전부가 한 명령. **스테이지 상태기계**: `built → canary(passive color, 트래픽 0) → [migration-validation 게이트] → live(active flip) → previous(직전 active = 즉시 롤백 타깃)` — blue/green 이 이미 canary/live/previous 3스테이지 제공. **migration-validation (안전 척추)**: 스키마 마이그레이션 포함 릴리스는 flip 전 prod DB 의 ephemeral clone 에 마이그레이션 적용·검증 (`axe drill`/backup 스냅샷 재사용). blue/green 이 **DB 공유** → 깨지는 스키마 변경은 카나리 *불가능* (green 용 마이그레이션이 blue 즉시 오염). 이 게이트가 blue/green 이 못 막는 유일한 위험 차단. 대상: blueprint 대기 migration 2개 (add_user_entra_oid, add_entity_legal_name), hive alembic. **rollout (additive)**: 전부 기존 동작과 공존 → `--dry-run` + passive-color 카나리(flip X) 검증 → cutover(기본값 flip + 가드 활성, escape hatch 포함) 게이트 → 되돌림 가능, 진행 중 WIP 보존. **채택 안 함**: 별도 staging *환경* (staging.axelabs.ai + 독립 DB/터널) — 단일 Mac mini 에서 새 실패유형 0 + 비용·parity 부담만, canary + migration-validation 이 실위험 커버. 작업추적 새 "스테이지" 축 — roadmap/backlog/updates 가 이미 작업 파이프라인. 본문: [/ops/runbook/deploy-ssot](/ops/runbook/deploy-ssot). 정합: [D-ops-16](/ops/decisions) (release-gate `axe ship`) · [D-matrix-3](/ops/decisions) (matrix SSOT) · [Blue/green deploy](/ops/runbook/deploy). | 2026-06-04 |
| D-axe-idp-1 | **Blueprint = 플랫폼 인가서버 (OIDC Provider) — 로그인 1회로 전 서비스** (2026-06-03 결정 → **Phase 1+2 LIVE 2026-06-04**; 현행 구현 SSOT [/architecture/platform-identity](/architecture/platform-identity)) — D-axe-cli-1 의 외부 인증을 "신규 Entra 앱" 대신 **Blueprint 중앙 관장**으로 (본질적 선택). Blueprint 가 Entra 를 상위 federate(인간 SSO 1회) + **자체 플랫폼 토큰 발행**(서명 JWT + JWKS + authorize/token/register + OIDC `.well-known`) → **모든 서비스가 Blueprint 신뢰**(iss=blueprint + JWKS 검증). 한 토큰이 전 서비스 동작 (서비스별 audience juggling 불필요). frame/cortex 의 per-service OAuth-RP 프록시(D-ops-14/15)를 Blueprint 하나로 **통합**(net-new 아닌 consolidation). 거버넌스(per-user/tenant scope·revoke·audit) Blueprint 중앙. **D-axe-cli-1 토큰모델 정정**: 인터랙티브=Blueprint 발행 토큰(Entra-direct/HS256 아님), HS256 부트스트랩은 헤드리스/cron 잔존. **보안 핵심 → 설계문서 + 신·구 병행 비파괴 cutover**. Phase: ①Blueprint OIDC-OP + `axe login`(loopback PKCE→Blueprint) + frame 가 Blueprint 신뢰(모델 증명) ②index/hive/cortex/matrix trust 이전 + proxy 폐기 ③거버넌스/감사 + headless device-code. **상태: Phase 1+2 LIVE (2026-06-04)** — Blueprint OIDC-OP(discovery·jwks·authorize·token·register·revoke) + RS256 키(vault) + `axe login` loopback-PKCE + frame·hive·cortex·index·matrix + Blueprint 자체 MCP **6개 서비스 전부 Blueprint 토큰 신뢰**(영속·e2e 검증). claude.ai→Blueprint OP 이전 + per-service 프록시 폐기, Phase 3(인가 중앙화·감사 UI·device-code·키 회전 자동화)만 잔여. | 2026-06-03 |
| D-axe-cli-1 | **외부/멀티에이전트 접근 = 고객 CLI (gh 모델) + 토큰 로그인, MCP 커넥터 대체** (2026-06-03 결정 → **LIVE**: 고객 CLI 배포·`axe login`·전 서비스 호출 가동; 사용자 매뉴얼 [/services/cli](/services/cli)) — 외부 인원이 서비스별 MCP 커넥터를 개별 등록하던 방식의 "느리고 불편" 본질 = ① 1인당 N개 커넥터 등록·인증 마찰 ② 커넥터당 도구 스키마 **100개+** (frame ~54 + HR ~40 + blueprint ~20 실측) 가 매 턴 컨텍스트 적재 → 모델 턴 지연·오선택. 해소 = **단일 CLI** (`~/axe-cli`). 근거: 셸 조합성(pipe/loop/batch 1회 실행, 모델 왕복 0 — MCP 도구는 고립 호출) + **에이전트 무관** (Claude Code·**Codex**·Cursor·Gemini CLI·cron·CI; CLI = syscall/ABI 층, vendor lock-in 0). **CLI-only** (웹 Cowork 용 Skill 폐기 — 외부 인원 Claude Code 표준화). **토큰** = `axe login --token` → OS Keychain → `Authorization: Bearer` (헤드리스/Codex 가능, 브라우저 OAuth 회피). 토큰 실체 = **HS256 부트스트랩 JWT** — frame `frame.<customer>.jwt_secret` 의 **이미-LIVE** 패턴 표준화; index 는 `INDEX_JWT_SECRET` 설정만 있고 **미배선**(Entra RS256 만 검증) → 배선 필요, hive/blueprint/cortex/matrix 도 PAT 경로 추가(P2). **배포 = Blueprint 설치 시 in-place 번들** (Blueprint=구동 시스템; `axe onboard` 가 테넌트 PAT 발급→Keychain = zero-touch 인증 + control-plane 자기설정) + standalone(curl/pip) 보조. **스코프 분리**: 운영자 `axe`(vault/deploy/secret/customers) ≠ 고객 CLI(업무 명령·토큰 스코프). 기존 `axe mcp` connector-catalog 는 외부용 대체. **현행 정정**: 토큰 모델은 [D-axe-idp-1](/ops/decisions) 로 갱신됨 — 인터랙티브 = Blueprint 발행 RS256 플랫폼 토큰(`axe login` SSO), HS256 부트스트랩은 헤드리스/cron 잔존. 배포 = `https://<tenant>.axelabs.ai/cli`(+`/cli.ps1`) served + `axe self-update`(매 실행 conditional GET). 잔여 backlog: headless device-code(`B-axe-cli-device-code`). | 2026-06-03 |
| D-ops-1 | folder-as-context — `.mcp.json` per folder | 2026-05-08 |
| D-ops-2 | JWT 평문 디스크 금지 — Keychain + launchctl env | 2026-05-08 |
| D-ops-3 | `sub` = identity, `permissions` = entitlement (분리) | 2026-05-08 |
| D-ops-4 | tool surface 분리 — operator/employee tools | 2026-05-09 |
| D-ops-5 | v1 permission matrix — read/write/close/admin | 2026-05-09 |
| D-ops-6 | end-to-end MCP test 자동화 | 2026-05-10 |
| D-ops-7 | `.mcp.json` is the spec location, NOT `.claude/settings.json` ⚠️ | 2026-05-11 |
| D-ops-8 | vault-per-entity canonical distribution | 2026-05-13 |
| D-ops-9 | Vaultwarden 가 canonical secret store | 2026-05-14 |
| D-ops-10 | Phase 1 SSO via Vaultwarden OIDC (Timshel) | 2026-05-14 |
| D-ops-11 | Timshel digest pin (mainline 무한 미지원 회피) | 2026-05-15 |
| D-ops-12 | sso_nonce manual SQL fix | 2026-05-15 |
| D-ops-13 | external_id mapping (Microsoft oid ↔ Vaultwarden user) | 2026-05-15 |
| D-ops-14 | frame OAuth-RP (Microsoft Entra ID direct) + Blueprint /onboard | 2026-05-17 |
| D-ops-15 | OAuth proxy 시도 (dormant — Anthropic Connector 한계로 미사용) | 2026-05-21 |
| D-ops-15.5 | axe.axelabs.ai 도메인 검증 + Application ID URI 형식 URL | 2026-05-21 |
| D-ops-16 | docs drift 강제는 release-gate (`axe ship`), pre-commit hook 채택 X | 2026-05-21 |
| D-ops-17 | secret = vault SoT + deploy-time pull (`customers.yaml` 매니페스트), runtime vault 호출 X | 2026-05-21 |
| D-ops-18 | compose env_file 단일 출처 (environment block 의 secret 중복 금지 + 하위 dir compose 의 `.env` symlink) + `axe secret pull` merge-mode (config 보존) | 2026-05-21 |
| D-ops-19 | Azure secret rotation = az cli + `--append` (overlap window) + app owner 운영자 명시 prereq | 2026-05-21 |
| D-ops-20 | **7-stack 응집** — service 별 1 stack 으로 통합 (proxy/console/docs/tunnel 분산 → frame·hive·blueprint·artemis·mysrt·vault·axelabs). cross-cutting infra (caddy/tunnels/docs/console) = `axelabs` stack. Portainer 12 stack → 7 stack. 본질·퀄리티·안전 우선. | 2026-05-22 |
| D-ops-21 | **`frame register-entity` 자동 chart seed** — entity 등록 시 표준 chart (KSME/KGAAP/KIFRS) 자동 apply. 이전엔 별도 `apply-chart` 누락 시 모든 분개가 `account_code not found` 로 실패하던 운영 함정 해소. | 2026-05-22 |
| D-ops-22 | **frame 펀드 도메인 지원** — `shared.entity` 에 `entity_kind` (corporate/kip/kvf) + `fund_meta` JSONB + `closed_at` 컬럼 ADD. `shared.entity_relationship` 확장 (`ownership_numerator/denominator/unit` — % 폐기, `kind` ENUM 에 `gp_managed_fund`/`lp_invested_fund` 추가). `shared.cross_journal_link` 신설 (AXEV↔fund mirror 분개 pair 무결성). `register-entity` CLI 에 `--kind`/`--fund-meta` flag. AXEV 창업기획자 산하 KIP/KVF 다수 결성 수용. 회계 원칙: AXEV↔fund = K-IFRS 1110 통제 미달 → 운용 관계 (parent-child 아님). `frame_meta` schema 신설 폐기 (기존 `shared` 확장이 자연 + 명명 충돌 회피). alembic `0008_shared` migration + frame-mcp blue/green rebuild. | 2026-05-22 |
| D-ops-23 | **frame RFC 9728 resource-level metadata path fix** — `/frame/mcp/.well-known/oauth-protected-resource` (application_id_uri 의 sub-path, RFC 9728 정확 path) 가 frame middleware 의 unauthenticated allowlist 누락으로 401 반환. claude.ai Connector / Anthropic MCP client 가 본 path fetch 시도 → OAuth challenge metadata 못 받음 → Reconnect silently 실패. `9b43845 feat(mcp): OAuth-RP dispatch + RFC 9728 endpoint` 시점부터 latent, D-ops-22 deploy (frame-mcp blue/green rebuild) 가 stored token invalidate 하면서 발현. fix: `http_server.py` 의 `_PUBLIC_PATHS` + `inner.router` 에 resource-level path 추가 (handler reuse `_oauth_protected_resource`). 양쪽 path (server-level `/frame/.well-known/...` + resource-level `/frame/mcp/.well-known/...`) 모두 200. commit `4f5dfcc`. | 2026-05-22 |
| D-ops-24 | **Vaultwarden SSO + Microsoft Entra ID — `email_verified` 면제** — Entra ID id_token 에 `email_verified` claim 부재 → Timshel fork 가 "Your provider does not send email verification status. ... `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION`" 오류로 거부. 첫 employee SSO 시도 (`taehun.kang@axellc.com`, 2026-05-22) 에서 발현. fix: `/Users/axe/.axe/vault/docker-compose.yml` 의 `axe-vaultwarden.environment` 에 `SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true"` 추가. SSO_AUTHORITY 가 single-tenant URL (`122fb574-...`) 이므로 외부 tenant 사용자 진입 불가 → 추가 검증 면제 안전. + Vaultwarden UI 함정 (이메일-first 기본 화면이 SSO Identifier 입력 칸 가림) 은 `/vault/#/sso` 직링크 안내 (troubleshooting.mdx). | 2026-05-22 |
| D-ops-25 | **MCP service 의 startup probe 강제** (D-bp-mcp-3 cross-cutting 일반화) — 모든 DB 종속 MCP service 의 Starlette lifespan 안에서 첫 traffic 받기 전 `SELECT 1` probe 실행. broken DB / malformed URL 시 lifespan fail → uvicorn 시작 실패 → healthcheck never green → `axe deploy SERVICE-mcp` blue/green swap 자동 거부 (broken container 가 active promote 못 됨). `/health/ready` endpoint 가 있어도 그건 request-time 검증이라 부팅 직후 broken DB 가 active 가능. **현재 상태**: blueprint-mcp ✅ 적용 ([PR #353](https://github.com/soohunkang/blueprint/pull/353)). **frame-mcp ✅ 적용 (2026-05-22, commit `3c7ca6a`)** — `src/frame/mcp/http_server.py` 의 outer Starlette lifespan 을 `@asynccontextmanager` wrapper 로 변경 (sync `Engine.begin()` + `SELECT 1` + log + `async with inner.router.lifespan_context(...)` bridge). startup log `"startup probe: DB reachable, schema accessible (D-ops-25)"` verified. hive-mcp **미적용** — frame pattern 1:1 미러 (5-10 분, `hive/src/hive/mcp/http_server.py`). mcp-server-checklist § 8 #16 으로 항구화. | 2026-05-22 |
| D-ops-26 | **Vaultwarden JIT user provisioning 활성** — D-ops-24 후속 incident. SSO 인증 통과 후 신규 user (`jinwoo.han@axellc.com`, 2026-05-22 10:01) 가 `/#/set-password-jit?identifier=AXE` 페이지에서 "An error has occurred. Failed to retrieve the invitation" 차단. vaultwarden log `Failed to retrieve the invitation` from `vaultwarden::api::core::accounts`. 원인: `SIGNUPS_ALLOWED=false` 가 JIT user 생성 차단 → invitation fallback 도 (`INVITATIONS_ALLOWED=false`) 없음 → 양쪽 모두 닫혀서 신규 employee 영구 차단. fix: `SIGNUPS_ALLOWED: "true"` 단독. `INVITATIONS_ALLOWED=false` 유지 (manual invitation flow 사용 안 함, JIT 로 통일). **D-ops-27 (operator 결정) 와 함께 읽을 것** — `SSO_ONLY` 는 false 유지 (emergency MP fallback 보존). | 2026-05-22 |
| D-ops-28 | **bw CLI 다운그레이드 2025.7.0 + 업스트림 vault migration 백로그** — 한진우/강태훈 AXE org 멤버 초대 자동화 시 brew 의 bw CLI 2026.4.1 이 `TypeError: Cannot read properties of null (reading 'toWrappedAccountCryptographicState')` 로 login 단계 crash. [bitwarden/clients #19413](https://github.com/bitwarden/clients/issues/19413) 와 동일 — CLI 2026.x 가 server ≥ 2026.x API 응답 필드 요구하는데 Timshel fork 의 최신 stable 은 `1.34.1-6` (upstream Vaultwarden 1.34.1, 2025-07-15 빌드). Timshel ghcr 에 1.34 이상 image 없음 (1.36.0 git tag 만 존재). fix: `brew uninstall bitwarden-cli && npm install -g @bitwarden/cli@2025.7.0` (Timshel 1.34.1-6 동시기). `/Users/axe/.axe/vault/invite-members.sh` 가 멤버 invite + auto-accept + confirm 한 번에 처리. **상위 결정 변경**: D-ops-11 의 "Timshel digest pin" 사유 (mainline Vaultwarden SSO 미지원) 가 2026-05 upstream 1.36.0 의 native SSO 도입으로 해소 → backlog `B-vault-upstream-migration` 등재 (dani-garcia/vaultwarden:1.36.0 으로 이전, schema migration + SSO 환경변수 매핑 + 데이터 보존 검증). 이전 완료 후 bw CLI 최신 line 복귀 가능. | 2026-05-22 |
| D-ops-27 | **Vaultwarden `SSO_ONLY=false` 유지 + `ORGANIZATION_INVITE_AUTO_ACCEPT=true`** (operator setup 마무리, 2026-05-22). (a) **SSO_ONLY=false**: D-ops-26 초안의 `SSO_ONLY=true` 폐기. Microsoft Entra 장애 / SSO 설정 오류 시 operator 가 master password 로 emergency 진입 가능해야 함 (`axe ship docs` 와 동일 원칙 — single-vendor lock-out 회피). trade-off: 외부에서 `axe.axelabs.ai/vault` 직접 password 가입 가능 (org collection 접근 X). mitigations: SSO_AUTHORITY single-tenant 가 외부 tenant SSO 진입 차단 + org member invite operator-only. **TODO** (backlog): 비정상 user row INSERT 감지 cron (`B-vault-anomaly-cron`). (b) **ORGANIZATION_INVITE_AUTO_ACCEPT=true**: org member 초대 시 사용자 측 "Accept invitation" 클릭 단계 자동 통과. operator 의 "Confirm" (org key 를 user public key 로 암호화 전달) 만 남음. 안전: invite 자체는 operator-only 이고 user pool 은 SSO 통과한 tenant user 만 가능. | 2026-05-22 |
| D-ops-29 | **Dual-identity for 강수훈 — `ai@axellc.com` (automation, AAD `7f110c52-...`) + `soohun.kang@axellc.com` (human Owner, AAD `e35e6778-...`)** — founding operator 가 두 identity 보유. 분리 원칙: (a) **`ai`** = 자동화 / bot / cron / agent identity. Graph token 발행, 직원 proactive DM, frame/hive agent run. 사람 손작업 X. (b) **`soohun.kang`** = 본인의 사람 작업 identity. Teams 응답, vault item 등재, blueprint admin UI. Vault AXE org 에서 둘 다 Owner + access_all=1 (DB UPDATE `users_organizations.atype=0, access_all=1` 적용 2026-05-22). Blueprint User.role 둘 다 admin. **이유**: bot 메시지가 human sender 로 표시되거나 vice versa 시 incident response 추적 혼란 (D-ops-23 의 mcp_client_invalid 디버깅 사례 참조). audit trail 명확화 + 신원 misuse 회피 + sender identity propagation 본질 원칙. | 2026-05-22 |
| D-ops-30 | **Bot proactive DM endpoint — `/api/admin/broadcast-dm`** (blueprint) — operator (ai@axellc.com identity) 가 직원 N명에게 동일 메시지 일괄 1:1 Teams DM. Auth: `Bearer <CRON_SECRET>`. Body: emails 배열 + text + contentType (text|html). 각 email 마다 (1) User.aadObjectId lookup → (2) Graph POST /chats oneOnOne (bot + user, idempotent — same pair 반환) → (3) POST /chats/CHAT_ID/messages. Pattern: `cron-failure-alert.ts:postAlertToAdmin` 재사용 (single → fan-out). 사용 사례: vault onboarding 안내, 플랫폼 공지, 운영자 broadcast. agent-context reply 는 `graph_chat_message_post` (Blueprint MCP) 그대로 사용. | 2026-05-22 |
| D-frame-lottecard-scraper | **롯데카드 거래내역 자체 스크래퍼 + 멀티카드 적재** (2026-05-29) — CODEF 등 외부 집계 API 미사용, 자격증명으로 롯데 법인 포털 직접 로그인하는 헤드풀 Playwright 스크래퍼 (`frame scrape <source>` 제네릭 + 롯데카드 첫 커넥터, host-only). 법인관리자 ID/PW (웹 보안키패드) → 승인내역 **개인정보 미포함(=이메일/인증 생략)** 신청 → 비동기 생성 → CP2 .xls 다운로드 → ingest. 법인전체 export 가 법인의 모든 카드를 담으므로 `ingest_lotte_card` 를 **행별 계정 resolve** 로 수정 (마스킹 `5105-****-****-*042` ↔ 기존 실번호 계정 `5105-5400-0199-9042` 와 와일드카드 매칭하여 링크, 실번호 보존; 신규 카드는 새 계정). 비밀 = `frame/axe/lotte-portal-{username,password}` (vault, host-only — MCP 컨테이너 제외). 스케줄은 DB-driven ([D-frame-scrape-schedule](/ops/decisions)). 함정: axec id=3 의 기존 701행이 혼합 카드 (과거 첫-행-버그) — 별도 정합성 정리 대상. | 2026-05-29 |
| D-frame-scrape-schedule | **스크래퍼 주기 = DB(MCP 조정) + launchd 단순 tick** (2026-05-29) — 주기를 plist 하드코딩하지 않고 `shared.scrape_schedule`(KST cron) 에 두고 MCP `set_scrape_schedule`(raw cron 또는 daily/weekly/monthly 프리셋) 로 조정. MCP 컨테이너는 호스트 launchd 를 못 건드리므로 launchd 는 `com.frame.scrape-tick`(08–21시 매시 :15) 단순 tick → `frame scrape-due` 가 croniter 로 due 판정해 해당 source 만 실행 + last_run/next_due 갱신. cron 은 KST (anchor=last_run else created_at → 갓 만든 weekly 는 첫 진짜 fire 에 실행, 첫 틱 즉발 X). croniter+Playwright=host-only(`schedule.py`), cron 컴파일=pure(`cron.py`, 컨테이너 공용). 마이그레이션 `0009_shared`. 신규 MCP 도구 3개 → frame-mcp rebuild 필요. **SLA(다운타임 catch-up)**: 놓친 실행은 복구 후 1회 따라잡기 + look-back 을 now−last_run(상한 365일)까지 확장해 그 기간 backfill (row_hash dedup, 손실 0), `RunAtLoad` 부팅/wake catch-up, MCP `overdue` 가시화. seed: lottecard/axec weekly Mon 08:10. | 2026-05-29 |
| D-ops-33 | **`axe vault bootstrap {customer}` CLI 신설** — D-ops-24/26/27 4-key 적용을 코드로 항구화. customer-onboarding.mdx 의 D+1 수동 단계를 1 명령으로 축소. 동작: SSH (or local for axe) 로 `/Users/{ssh_user}/.axe/vault/docker-compose.yml` 읽음 → 4 key 진단 (idempotent) → 누락만 patch (anchor: `SSO_SIGNUPS_MATCH_EMAIL`) → force-recreate `axe-vaultwarden`. Default = dry-run, `--apply` 로 실 패치. **검증**: `axe vault bootstrap axe` (이미 적용 — "bootstrap 불필요" 정확 detect) + `axe vault bootstrap realchoice` (file 부재 시 깔끔한 에러). 후속 ([B-onboard-cli-vault-bootstrap](/ops/backlog)): Vault Organization 자동 생성 + 첫 admin invite (현재 web UI 수동). | 2026-05-22 |
| D-ops-31 | **axep entity 신설 (액스파트너스 유한책임회사)** — axe customer 의 3번째 entity 추가 완료 (2026-05-22). frame `register-entity --id axep --kind corporate --accounting-standard ksme` (chart auto-seed 54/59 accounts, D-ops-21) + hive `register-entity --id axep` (frame mirror). `customers.yaml`: entities 배열 `["axec","axev","axep"]` + frame/hive PII passphrase secret 매니페스트 2개 (`frame/axe/pii-passphrase-axep`, `hive/axe/pii-passphrase-axep`) 추가. user_entity_map: ai/cfo/soohun 만 `["axev","axec","axep"]` (axep 참여), taehun/jinwoo 은 axep 제외 유지. **Deploy 완료**: PII passphrase 2개 vault push (`axe secret push`) + frame blue/green swap (active=green:3711, blue:3710 next passive) + hive sequential force-recreate (green → 6s → blue). axec/axev 영향 0 (PII passphrase entity별 분리 + lazy-load). 자동화: `/Users/axe/.axe/vault/deploy-axep.sh`. **부수 발견 함정**: `axe secret push` 가 `BW_SESSION` env var 무시 + macOS Keychain (`axe.vault.session` / `ai@axellc.com`) 만 읽음 → 자동화 시 `security add-generic-password -U` 로 keychain 갱신 단계 필수. | 2026-05-22 |
| D-ops-34 | **Onboard 1-input 모델 — Phase A (vault unlock = 세션 prerequisite) + Phase B (umbrella 명령 1 줄)** — 신규 customer (realchoice 등) 배포 시 운영자 D-day 입력 = **axe 명령 1 줄**. 가정: vault 가 미리 unlock 돼 있어야 함 (`bw unlock --raw` → Keychain `axe.vault.session` cache, 8시간). 이후 모든 `axe secret`/`axe onboard`/`axe deploy` 가 keychain BW_SESSION 자동 사용 — D-day 추가 prompt 0. **Phase B umbrella** = `axe deploy customer {customer} --from-pack pack.json --apply` ([B-onboard-umbrella](/ops/backlog)) — 내부에서 ingest + onboard (cloudflared+frame) + deploy blueprint + deploy hive 순차. **거부**: cron 기반 `bw unlock` keep-alive (vault 상시 unlocked = 머신 침해 시 전체 노출 = blast radius 큼). 권장 모델 = "출근 시 비번 1 회 입력 → 하루 1 입력" 보안/편의 최적점. customer IT 측 mirror = `az login` 1 회 + `axelabs-bootstrap.sh` 1 줄 (docs.axelabs.ai 만으로 자력 — [D-onboard-bootstrap-publish](/ops/decisions), [/partner/registration §Option A](/partner/registration#option-a--cli-1-줄-권장)). | 2026-05-23 |
| D-ops-32 | **Vault collection 구조 v1 — 6 collection** (D-ops-9/27/29 종합 설계) — 기존 4 collection (Default empty, frame-jwt-axec/axev D-ops-14 잔재 each 1 item, frame-jwt-operators empty) → 6 collection: (1) **Platform — Service Secrets** (37 customers.yaml secrets, ai+soohun only) (2) **Platform — Infrastructure** (Cloudflare API, vault ADMIN_TOKEN raw, GitHub org admin, DNS, cert; ai+soohun only) (3) **AXEC** (4명 RW — KB/홈택스/Azure axec/세무사 access) (4) **AXEV** (4명 RW — KB/KSD/금감원/GP-LP portal/investee data room) (5) **AXEP** (ai+soohun only — GP 단독 출자/운영) (6) **Shared Tools** (4명 RW — Notion/Slack/공통 SaaS/도메인 admin/메일 alias). 기존 frame-jwt-axec + frame-jwt-axev 의 2 item → Platform — Service Secrets 로 이동. 기존 Default + frame-jwt-operators 삭제. 구체 마이그레이션은 운영자 web UI 권장 (SQL/bw 직접 manipulation 은 Vaultwarden cache 충돌 위험). 외부 인력 (회계사/LP) 합류 시점에 entity-level read-only 분리 검토. **Progress (2026-05-26)**: 6 collection 전부 신설 ✅. AXEC (`5b13e7e4...`) + AXEV (`6eb223cb...`) + AXEP (`89de0ab9...`) + Platform — Service Secrets (`1d794d29...`) + Platform — Infrastructure (`50f2705f...`) + Shared Tools (`68197247...`). 권한 부여: AXEC/AXEV/Shared Tools 4명 RW (ai+soohun manage=true / taehun+jinwoo manage=false), AXEP + Platform 둘은 ai+soohun only. `bw create org-collection` 자동화 검증 — D-ops-32 가 명시한 "운영자 web UI 권장" 은 완전 우회 가능. **영향 평가**: axe CLI `_vault_get` 가 name 기반 (`bw get password <name>`), service deploy 가 `.env` 에 inject — collection 추가/이동/삭제로 MCP connector 깨지지 않음 (running connector 영향 0, 다음 deploy 시 lookup 도 무관). **AXE org role 확인**: ai@ + soohun.kang 모두 type=0 (Owner), taehun + jinwoo type=2 (User). **남은 작업 (별건)**: (a) 운영자 ai@ 의 personal vault 의 service secrets (axe secret list 의 ~37 path) 를 Platform — Service Secrets 로 이전 — 현재 ai@ only, bus factor 위험. (b) Default 의 3 items (`axe-macmini local: soohun.kang`, `axe-macmini local: taehun.kang`, `github PAT: ai@axellc.com`) 검토 — github PAT 은 Platform — Infrastructure, 나머지 2 은 personal vault 가 적절. (c) frame-jwt-axec/axev/operators 의 2 item (`taehun-kang-axec-2026-05-17`, `ai-operator-2026-05-16`) 검토 후 적절한 신설 collection 으로 이동. (d) 옛 4 collection (Default + frame-jwt-*) 삭제. → 신규 backlog [B-vault-collection-migration-v1](/ops/backlog). **2026-05-26 권한 정정 fix 적용 완료**: `bw create org-collection` 의 users 배열 처리 server-side 함정 (Owner skip + manage=0 강제 — 자세한 root cause = [B-vault-org-perm-3-quirks](/ops/backlog)) 으로 6 collection 모두 ai+soohun 권한 누락 또는 partial 이었음. SQLite 직접 `users_collections` 일괄 INSERT/UPDATE + ai+soohun-only collection 의 taehun/jinwoo entry DELETE 로 본 결정의 권한 모델 (4명 RW vs ai+soohun only) 그대로 정합 복원. cipher delete/move 는 `*/admin` HTTP endpoint 직접 호출이 정공법 (bw CLI 의 cipher.permissions.delete client-check 우회). | 2026-05-22 |
| D-nemotron-1 | **`/services/nemotron-personas` 카탈로그 등재 (placeholder)** — 트루비아 측 기존 자산 (Docker SSE :8771, NVIDIA Nemotron-Personas 데이터셋 1M+ 페르소나 풀) 이 docs 미등재였던 갭 해소. 보고서 Q5 confirm 후속 (2026-05-23 RE^2). 본격 본문은 D+14 (2026-06-15) 트루비아 측 라이선스/origin 회신 후. 라이선스 cross-customer OK 면 공식 카탈로그 + Blueprint MCP registry, X 면 realchoice 전용 (`customers.realchoice.private_services[]` 신설 검토). 현 시점 docs 노출은 가시성 + 운영자 SPOF 회피 목적. | 2026-05-23 |
| D-magnet-tenant-map-1 | **`customers.yaml > customers.{customer}.service_tenant_map` 신설** — magnet 의 service-internal RLS `tenant_id` (integer) 와 AXE customer ID (string) 의 매핑 SSOT. `MAGNET_TENANT_ID=1` (realchoice) 유지, 향후 axe 가 magnet 사용 시 `tenant_id=2` 부여로 충돌 회피. 트루비아 측 보고서 Q6 confirm 후속 (2026-05-23 RE^2). `/services/magnet#tenant-id-mapping` 본문. 향후 stream / 기타 service-internal id 도 동일 패턴 확장. axe ship 이 manifest 에서 자동 주입은 `B-magnet-tenant-env-injection` 후속. | 2026-05-23 |
| D-onboard-bootstrap-publish | **`axelabs-bootstrap.sh` 를 docs.axelabs.ai 의 raw 로 노출** — 신규 customer IT 가 docs link 만 받고 자력 완료 가능한 상태 (별도 안전채널 메시지 + `realchoice_entra_id_setup_v4.md` 의존 폐기). `/Users/axe/.axe/bootstrap/axelabs-bootstrap.sh` → `/Users/axe/axelabs-docs/public/axelabs-bootstrap.sh` 복사 (17749 bytes, SHA-256 `7d2d607136e5387c6c9368bf8529d4ec0e219f8eb7bab45b57590a00838210cc`). partner/registration §Option A 가 docs 안에 다운로드+검증+실행+회신을 모두 포함하도록 재작성. 함께 신설: partner/macmini-prep.mdx (Tailscale 가입 + SSH key 회신 + 절전 OFF + Docker 6 단계) + partner/domain-prep §A (axelabs.ai zone = 운영자 책임, 귀사 IT 1 분 한 줄 전달) §B (귀사 corporate domain TXT 가이드) + partner/handoff JSON pack 양식 (`axelabs-bootstrap/v1` schema) + partner/index 의 4-step 흐름. customers.yaml realchoice.sso.apps 에 blueprint 슬롯 사전 추가 (bootstrap.sh 의 apps.blueprint 출력 흡수 대비). 운영자 측 1-shot umbrella ([D-ops-34](/ops/decisions)) 는 여전히 backlog — 운영자 D-day 명령 수 5→1 줄이는 작업은 별도 추진. customer-facing 의 docs-only 자력 완료는 본 결정으로 달성. | 2026-05-23 |
| D-ops-41 | **SSH client-side automation: NSSM/서비스 폐기, user-context Scheduled Task + VBS wrapper 채택** — `/onboard/ssh-access` 실전 셋업 (Soohun Kang Windows 11, 2026-05-27) 도중 6 함정 발견 후속. (1) cloudflared TCP forward 와 token renewal 모두 **user-context Scheduled Task** 로 통일. TCP forward 는 **VBScript wrapper 호스팅** 으로 0-flash hidden + 무한 재시작 보장 (`wscript.exe` + `Do ... sh.Run ..., 0, True; WScript.Sleep 5000; Loop`). Token renewal 은 **self-rescheduling PS1** — JWT `exp` claim 파싱 후 `exp - 5min` 에 한 번만 fire (24h 주기 task fire 2~3회), `Set-ScheduledTask` 가 자기 trigger 재예약. (2) **거부된 대안**: (a) NSSM/SYSTEM 서비스화 — JWT 토큰 user-scoped (`%USERPROFILE%\.cloudflared\`) vs SYSTEM 의 `%SystemRoot%\System32\config\systemprofile\.cloudflared\` 격리 → 무한 `failed to acquire app token lock` 루프 + SYSTEM 의 desktop session 부재로 자체 login 도 무한 "Waiting for login..." (함정 #10). (b) NSSM 사용자 계정 실행 — 패스워드 평문 저장 + Microsoft 계정/Windows Hello 사용자 UX 저하 + 계정 패스워드 변경 시 서비스 정지. (c) Cloudflare Service Token — [D-ops-29](/ops/decisions) dual identity audit 와 충돌 (SSO email log 없어짐). (d) Access app `session_duration` 30d 연장 — 갱신 빈도만 줄어들 뿐 자동화는 별개. (3) **PowerShell `-WindowStyle Hidden` 폐기** (daemon 류) — Windows 가 hidden flag 적용 전 수십 ms 콘솔 노출 → 매 task fire 마다 깜빡임 잔상 (함정 #13). `wscript.exe` 호스팅이 0-flash. (4) **Task `-RestartCount` 의존성 0** — FAILURE (non-zero exit) 에만 발동이라 cloudflared 정상 종료 (exit 0) 시 silent 중단 → 며칠 후 SSH 끊김 (함정 #14). VBS wrapper 안의 `Do ... Loop` 가 어떤 종료 코드든 5초 뒤 재기동. (5) **Renewal task `-RunLevel Highest` 필수** (함정 #11) — `Set-ScheduledTask` 는 elevated 권한 필요, Interactive 만으로 `Access is denied`. (6) **SSH use case 의 active logon session 전제** — "logon 전에도 살아있음" 이 필요 없음 → user-context Scheduled Task 가 NSSM 보다 깔끔 (패스워드 저장 0 / 토큰 캐시 자동 공유 / 본인 끌 UI 0 / 가시적 창 0). **결과 = 사람 클릭 0회 조건부** — Microsoft "로그인 상태를 유지" sticky session 켜져 있으면 renewal 시 SSO 자동 통과. Entra conditional access 가 sticky 차단하면 갱신 시점 1회 클릭 필요 (정책 영역, client 우회 불가). **검증 환경**: Windows 11 + cloudflared 2026.5.1 + OpenSSH for Windows 9.5p2 / LibreSSL 3.8.2. **참조**: [/onboard/ssh-access § TCP forward 자동화](/onboard/ssh-access#tcp-forward-자동화-windows-user-context-scheduled-task--vbs-wrapper) / [§ Token 자동 갱신](/onboard/ssh-access#token-자동-갱신-windows) / [§ 함정 #9~#14](/onboard/ssh-access#함정-정리). | 2026-05-27 |
| D-ops-40 | **axe.3 vault release plan — 2 patch (Owner skip + cipher.permissions) + KDF rotation procedure + `axelabs-ai/vault` GHA pipeline 구축** — [D-ops-37](/ops/decisions) axe.2 deploy 후 발견된 [B-vault-org-perm-3-quirks](/ops/backlog) (3 함정) + [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) 의 모든 근본 진단 (2026-05-26 timshel/vaultwarden source clone read-only 진단). **Patch A — 함정 1 (Owner skip on collection user save)**: `src/api/core/organizations.rs` 의 **2곳 동일 fix** — `post_organization_collections` (line 522-524) + `post_organization_collection_update` (line 685-687) 에서 `if member.access_all { continue; }` 3 라인 제거. 결과 = Owner role 도 `CollectionUser::save` 명시 entry 생성 → 권한 체크 정합 + cipher.permissions.delete 계산 정상. **Patch B — 함정 3 (cipher.permissions field missing)**: `src/db/models/cipher.rs::to_json` 함수 line ~239 부근 `"edit"` / `"viewPassword"` 옆에 `"permissions": {"response": null, "delete": <bool>, "restore": <bool>}` 추가. `<bool>` 계산 = `Membership::find_by_user_and_org(user_uuid, org_uuid).await` 결과의 `atype <= Admin` OR `cipher.user_uuid == Some(user_uuid)` OR (`get_access_restrictions().manage == true`). Bitwarden mainline schema backport. **Patch C — 함정 2 (cipher edit collectionIds silent partial-revert)**: **patch 안 함** — vaultwarden bug 아닌 mainline design (collectionIds 변경은 `POST /ciphers/{id}/collections` 별도 endpoint). 운영 룰: bw CLI 의 `bw edit item` 으로 collectionIds 변경 불가, 대신 `bw move` 또는 `/admin` endpoint 직접 호출 ([B-vault-org-perm-3-quirks](/ops/backlog) 의 운영 임시 룰 영구). **Patch D — SSO→MP wrapped MP**: **patch 안 함** — axe.2 의 response shape backport 가 이미 충분. 기존 user (axe 측 4명) 의 user.akey 가 옛 KDF/wrapping schema 로 생성된 게 원인 (realchoice 측 신규 가입 user 는 axe.2 적용 후라 새 schema, 정상). **Fix = KDF rotation runbook** (운영자 셀프, patch 영역 외): 각 user 가 MP 단독 로그인 → Settings → Account → Security → Master password change (same value OK) + KDF type PBKDF2 → Argon2id → save → vault items re-encrypt → logout → SSO→MP 정상. axe 측 4명 (ai/soohun/taehun/jinwoo) 차례로. realchoice 측 영향 없음. **Build pipeline (axelabs-ai/vault repo, 현재 빈 상태)**: `build/build.sh` (clone timshel `1.34.1-6` tag + `patches/*.patch` 일괄 git apply + `docker buildx --platform linux/amd64,linux/arm64 --push ghcr.io/axelabs-ai/vault:<tag>`) + `.github/workflows/build.yml` (push.branches=main 자동 빌드 `main-<sha7>` rolling tag + `workflow_dispatch` input tag 로 release 빌드 `1.34.1-6-axe.3`) + `build/patches/0001..0004.patch` (axe.2 의 prelogin/password + AccountKeys + axe.3 의 organizations Owner-skip + cipher permissions). **Deploy 흐름**: (1) `axelabs-ai/vault` repo 의 build/ + workflows/ + patches/ commit. (2) workflow_dispatch → GHCR push. (3) `axe-macmini`: `docker compose pull vaultwarden && docker compose up -d` (compose.yml image 라인 = `ghcr.io/axelabs-ai/vault@sha256:<new-digest>` 로 핀, [D-ops-37](/ops/decisions) 의 axe.2 ImageID 주석 → axe.3 digest 로 갱신, 이전 digest rollback 주석 보존). (4) `realchoice-macmini`: 동일 pull + up (customer self-deploy, [D-ops-customer-sovereignty](/ops/backlog) 원칙). (5) axe + realchoice 양쪽 검증 = collection 생성 (Owner entry 명시 확인) + cipher delete (bw CLI 정상 동작 확인) + SSO→MP (KDF rotation 후 검증). **현금 비용 0** (GHCR + GHA runner 무료 한도 안, image 460MB, build ~15분/회). **운영자 시간 5~9h** (patch 작성 1h + build pipeline 1~2h + 빌드/검증 1h + KDF rotation 4명 30분 + Truvia 안내 30분). **AGPL 라이선스 고려**: Vaultwarden = AGPL-3.0. fork 의 patch 배포 시 source 공개 의무 — `axelabs-ai/vault` GHCR push public 가능 + source 공개 같이. private 유지 원하면 patch 만 사적 운영 (배포 안 함, GHCR private + 본인 macmini 만 pull). 본 결정 = **public 모드** (source 공개 = patch 공개, GHCR public, 라이선스 정합 + 무료 GitHub plan 한도 무관). **Progress (2026-05-26 KST, ~3.5h 실 진행)**: (i) `axelabs-ai/vault` repo PUBLIC 전환 ✅ (조직 정책상 GHCR package 는 PRIVATE 유지). (ii) realchoice-macmini 의 기존 build pipeline (build/build.sh + Dockerfile + README + workflow + 2 axe.2 patches) 그대로 vendor + axe.3 patch 2개 추가 ✅: **patch 0003** `src/api/core/organizations.rs` line 522 + 600 + 685 의 `if member.access_all { continue; }` 3 라인 제거 (plan 의 "2곳" 외 line 600 `post_bulk_access_collections` 추가 발견 — 동일 root cause, 3곳 모두 patch). **patch 0004** `src/db/models/cipher.rs::to_json` line 374 (실 line, plan 의 ~239 와 다름) 의 `edit/viewPassword` 옆에 `permissions: {response,delete,restore}` 추가 + line 161 의 `_` → `manage` 로 바꿔 `get_access_restrictions` 결과 reuse (DB round-trip 절약). `can_delete` = collection manage flag OR org Owner/Admin (`atype <= 1`) — `Membership::find_by_user_and_org` 한 번 호출. (iii) 로컬 docker buildx (arm64, --load) 검증 ✅ — 5분 (M-series + cache), Rust 컴파일 정합 확인 (image `axelabs-ai/vault:axe.3-localtest` ImageID `sha256:e1ac1617...`). (iv) commit `799015b` push to axelabs-ai/vault main → GHA `build-vault-image` 자동 트리거 → 13분 multi-arch build → ghcr.io/axelabs-ai/vault:main-799015b push ✅. workflow_dispatch 는 axe-labs-ai PAT 의 actions scope 부족으로 403 — 대신 `docker buildx imagetools create` 로 `1.34.1-6-axe.3` alias 발행 (rebuild 없이, 3초). **manifest list digest** `sha256:a26208a0794acbc9a2807379ffba33c7478dbe8d41daed24893f7916a55aeada` (arm64 `sha256:d97a6ed...`, amd64 `sha256:402eeac...`). (v) GHCR auth: PAT (read+write:packages+workflow) 발급 → **AXE vault `Platform — Service Secrets / ghcr-axelabs-ai-pull-pat`** 저장 ([D-ops-ops-17](/ops/decisions) SoT 원칙 — keychain 안 씀). docker login 패턴: `bw get password ghcr-axelabs-ai-pull-pat \| docker login ghcr.io -u axe-labs-ai --password-stdin`. (vi) axe-macmini deploy: data tgz + sqlite 백업 (`data-pre-axe.3-20260526-131744.tgz`, `db-pre-axe.3-20260526-131744.sqlite3`) → `docker compose pull && up -d` → 60s healthy. compose image 라인 = `ghcr.io/axelabs-ai/vault@sha256:a26208a...`. (vii) **검증 3건 in-production** (13:19 KST): **VERIFY 1** = `bw create org-collection` (test 용) → SQLite `users_collections` 에 `ai@axellc.com\|0\|0\|1` row 명시 insert ✅ (axe.2 = zero rows). **VERIFY 2** = raw `GET /api/sync` → org cipher 의 `permissions: {delete:true,restore:true,response:null}` 정확 emit ✅ (bw CLI 2025.7.0 출력은 `null` — 옛 client 가 unknown field 파싱 못해 표시 안 됨, 새 client ≥2026.4 는 정상 읽음). **VERIFY 3** = `bw delete item` (test cipher) rc=0 ✅ (cleanup 후). (viii) external `https://axe.axelabs.ai/vault/alive` HTTP 200 (0.28s). (ix) [B-vault-fork-build-pipeline](/ops/backlog) ✅, [B-vault-org-perm-3-quirks](/ops/backlog) ✅. (x) **잔여 = KDF rotation 4명 (ai/soohun/taehun/jinwoo)** — 운영자 본인 MP 입력 필수라 자동화 불가, [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) runbook 참조. realchoice 안내 별도 (customer-sovereignty — 직접 pull, vault 에서 PAT 받아감). **(xi) ai@ KDF rotation 검증 완료** (Master password change + rotate encryption key 만으로 SSO→MP unlock 정상 — KDF 변경 없이 akey 재wrap 만으로 충분 확인). **(xii) post-deploy 정책 layer 박제** ([/architecture/vault-policies](/architecture/vault-policies) 신설): **(a)** server env `SSO_AUTH_ONLY_NOT_SESSION=true` 적용 — SSO 가 인증만, session lifecycle = Vaultwarden 자체 30일 idle refresh token. trade-off = Entra 비활성/제명 ≤30일 lag (AXE 4명 규모 OK), benefit = SSO+MP 재입력 월 ~1회. **(b)** Org Policies 4종 API 적용 (Owner ai@ token 으로 PUT) — MasterPassword(min 12, complexity 1), PasswordGenerator(len 20), SingleOrg, PersonalOwnership. **(c)** MaximumVaultTimeout 시도 HTTP 400 — Timshel source 의 `// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)` 확인. Bitwarden commercial license 라 Vaultwarden 영구 미지원, `SSO_AUTH_ONLY_NOT_SESSION` 이 가용 영역 안 best 대체. 3 layer 모델 (server env + Org Policies + per-client preferences) 통합 박제. **(xiii) 4명 KDF rotation 전부 완료 → D-ops-40 전체 종결** (2026-06-04): ai (5/27) → soohun → taehun (5/29) → jinwoo (6/4) 순서. **본질 발견 = KDF type dropdown 변경 불필요** — Master password change + ☑ "Also rotate my account's encryption key" 체크박스 만으로 `user.akey` 재wrap 충분 (plan 의 "KDF PBKDF2 → Argon2id" 보다 간소). DB: ai 는 Argon2id (Phase B 까지 진행), soohun/taehun/jinwoo 는 PBKDF2 유지 — **kdf_type 안 바뀌어도 akey 재wrap 만으로 SSO→MP fix 동작 확인**. 4명 모두 SSO→MP unlock 정상. jinwoo last_login 5/23→6/4 점프 = 완료 신호 + 운영자 구두 확인. 운영 도구 부산물: [/onboard/vault-setup](/onboard/vault-setup) playbook (taehun 피드백 6건 반영) + [/ops/runbook/operator-broadcast](/ops/runbook/operator-broadcast) (Teams DM 1:N) + [/architecture/vault-policies](/architecture/vault-policies). [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) ✅. **D-ops-40 전 작업 (build pipeline + axe.3 patches + GHCR + deploy + 검증 3건 + 정책 layer + 4명 rotation) 종결.** | 2026-05-26 |
| D-ops-39 | **무료 Universal SSL 의 1-level wildcard 한계 = flat-hostname 컨벤션 강제 (ACM 유료 회피)** — Cloudflare 무료 plan 의 `*.axelabs.ai` Universal SSL cert 는 1단 서브도메인만 cover. 2단 (`ssh.axe.axelabs.ai`) 은 SAN 미일치 → edge default cert fallback → 클라이언트 `tls: handshake failure` (Windows: `SEC_E_ILLEGAL_MESSAGE 0x80090326`, macOS: `sslv3 alert handshake failure`). 강태훈 Windows 의 `cloudflared access login https://ssh.axe.axelabs.ai` (2026-05-25) 에서 발현, MAX agent 진단 (axelabs.ai chat) → 운영자 forward → 본 결정. **Fix**: 신규 1단 hostname `ssh-axe.axelabs.ai` (DNS CNAME → 동일 tunnel `d8efecdd`, ingress 규칙 clone, Access app `b903d8cd` self_hosted_domains 추가). 검증: `curl -v https://ssh-axe.axelabs.ai` → `*.axelabs.ai` cert SAN 매치 + Access 302 OK. 옛 `ssh.axe.axelabs.ai` 와 vestigial `*.axe.axelabs.ai` 와일드카드 (이번 한계 violation) 는 [B-ssh-axe-flat-hostname-sunset](/ops/backlog) 로 grace period 후 일괄 삭제. **거부된 대안**: (a) Cloudflare Advanced Certificate Manager $10/cert/월 — 1개 hostname 위해 유료 전환 부당. (b) `axe.axelabs.ai` 별도 zone 등록 — NS delegation/zone 이원화 overhead 과잉. (c) 강태훈 Tailscale 우회 — 일반 직원 권한 범위 외 + 권한 확장 정책 필요. (d) 클라이언트 cert 검증 우회 — 보안 위반 + 다음 직원도 같은 함정 재현. **컨벤션**: HTTPS 노출되는 모든 hostname 은 `{name}.axelabs.ai` 1단 (`ssh-axe`, `docs-axe` 등 flat). sub-context 필요하면 path (`axe.axelabs.ai/frame`). 본 룰 [/architecture/domains#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39](/architecture/domains#함정--universal-ssl-wildcard-의-1-level-한계-d-ops-39). | 2026-05-26 |
| D-ops-38 | **외부 service 2FA 는 vault item TOTP 필드, AXE 내부 vault 계정 자체 2FA skip (Entra SSO 가 MFA 역할)** — AXE 가 사용하는 외부 services (KB/홈택스/AWS/GitHub/카드사/세무사 SaaS/etc.) 의 2FA 코드 는 Vaultwarden item 의 `login.totp` 필드 (Vaultwarden 무료 지원, Bitwarden premium 의 self-host 등가) 에 저장 → vault unlock 시점에 client (web/ext/native) 가 자동 6-digit 생성. **AXE 내부 vault 자체 user 2FA 는 활성 안 함** — Microsoft Entra SSO 가 이미 MFA 역할 (Entra ID 의 conditional access + Authenticator app 정책). Vault 의 SSO→MP unlock 흐름이 동작하면 (현 [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) 해소 후) 그 자체가 multi-layer. **AI agent 자동화 활용**: Claude Code / axe CLI / 기타 운영 자동화가 외부 service 로그인 시 TOTP 필요한 경우 `bw get totp <item-name>` 또는 향후 `axe secret totp <name>` ([B-axe-secret-totp-cli](/ops/backlog)) 으로 vault 에서 자동 조달 — 운영자 phone 의존 제거 + bus factor 0. **운영 룰**: 외부 service 2FA 등록 시점에 (a) phone Authenticator 앱은 사용 안 함, (b) QR/secret/`otpauth://` URI 를 vault item 의 TOTP 필드에 직접 저장, (c) phone 분실/교체 시 손실 없음. **trade-off**: vault 가 단일 trust boundary — 뚫리면 외부 service 2FA 도 같이 뚫림. 따라서 vault 의 SSO/MP/(향후) 2FA layer 가 더 중요해짐. AXE 내부 vault 계정 자체 2FA skip 결정은 **Entra SSO 가 살아있을 때** 만 유효 — SSO 가 깨진 현재 상태 ([B-vault-axe.2-sso-mp-incomplete](/ops/backlog)) 에서는 MP single-factor 단계라 임시 위험 가시. SSO 흐름 복원 시점에 자동 mitigation. | 2026-05-26 |
| D-ops-37 | **AXE-local Vaultwarden fork build — `ghcr.io/axelabs-ai/vault@sha256:a26208a0794acbc9a2807379ffba33c7478dbe8d41daed24893f7916a55aeada` (axe.3, 2026-05-26)** — Bitwarden client >= 2026.4 (Chrome extension + bw CLI) 와 Timshel 1.34.1-6 fork 사이 두 protocol 불일치 영구 fix. (1) Bitwarden client 가 `/accounts/prelogin/password` 로 split — Timshel 은 legacy `/accounts/prelogin` 만 노출 → 404 → 로그인 시작 불가. (2) `/identity/connect/token` 응답에 새 wrapped-variant 필드 (`AccountKeys` / `MasterPasswordUnlock`) 부재 → "toWrappedAccountCryptographicState" throw → unlock 실패. 패치 2개 모두 dani-garcia/vaultwarden mainline 에서 backport. realchoice tenant 가 2026-05-25 자체 build & deploy → axe-macmini 는 본 일 (2026-05-26) realchoice-macmini local image 를 docker save/load 로 import → `axe-vaultwarden` recreate (ImageID `sha256:11536845...`, arm64). prelogin/password endpoint 200 OK 회귀 검증 완료 (이전 404). **2026-05-26 추가 (axe.3 ramp, [D-ops-40](/ops/decisions))**: axelabs-ai/vault GHA pipeline 정식 구축 + axe.3 patch 2개 추가 (organizations Owner-skip + cipher.permissions) → image 라인 = `ghcr.io/axelabs-ai/vault@sha256:a26208a...` (manifest list, multi-arch). [D-ops-11](/ops/decisions) immutable digest 핀 정합 복원. **rollback (axe.2)**: 로컬 tag `axelabs-ai/vault:1.34.1-6-axe.2`, ImageID `sha256:11536845...`. **rollback (pre-axe.2)**: `ghcr.io/timshel/vaultwarden@sha256:a5d5ed0366abd84cc7443a79e52ffe4774671648dca5e1a5412fd91b77d4f692`. **follow-up**: (a) ~~`axelabs-ai/vault` 에 build/patches/ + GHA workflow 실 셋업~~ ✅ ([B-vault-fork-build-pipeline](/ops/backlog) Done). (b) 패치 2개 Timshel upstream PR 제출 ([B-vault-axe.2-patches-upstream-pr](/ops/backlog)). [B-vault-d-day-traps-2026-05-25 #12](/ops/backlog) 의 client-side bw 2025.7.0 pin workaround 와 별도 — 본 결정은 server-side 영구 fix. **vault-next 18-24개월 roadmap 의 bridge** (별도). | 2026-05-26 |
| D-ops-35 | **`axe deploy hive {customer}` subcommand 신설** — frame blue/green 패턴 1:1 mirror. `cmd_deploy_hive` + `_hive_active_color` + `_hive_port` 헬퍼 + `HIVE_*` 상수 (compose, proxy=`axe-hive-proxy`, networks=`["hive_default","artemis_default"]`, alias=`hive-mcp`, drain=5s). 8-step: build passive → up -d → /health/ready 200 wait → add alias (양쪽 network) → caddy reload → edge verify (200/401 둘 다 정상 — JWTAuthMiddleware) → drain 5s → 옛 active alias 제거. argparse choices 에 `hive` 추가, dispatcher 분기. **검증**: `axe deploy hive axe --apply` 실 swap (blue:3810 → green:3811 active, edge `axe.axelabs.ai/hive/health: 200`, 무중단). hive 가 `axe ship hive` 의 manual_hint 만 남아있던 격차 해소. realchoice 6/1 deploy 가 진정 1-shot 가능 — `axe deploy customer realchoice --from-pack ...` umbrella ([D-ops-34](/ops/decisions)) 의 hive 단계도 자동. customer-onboarding.mdx 의 step 4 manual ssh 폐기. | 2026-05-23 |
| D-ksme-1 | **KSME seed 판관비 10개 보강** — 5210 급여 / 5211 임원급여 / 5212 잡급 / 5213 퇴직급여 / 5214 복리후생비_4대보험 / 5215 통신비 / 5216 광고선전비 / 5217 도서인쇄비 / 5218 교육훈련비 / 5219 감가상각비. 한국 법인 필수인 급여 계정 자체가 없던 본질적 결손. | 2026-05-22 |
| D-frame-3 | **`cash_flow_statement` 신규 계정 fallback 분류 (type + code prefix) — 명시적 dict 우선, 미매핑 계정만 규칙 적용** — CF 분류가 hardcoded dict (`INVESTING_ACCOUNTS`/`FINANCING_ACCOUNTS`/`OPERATING_*`) 라 신규 계정(1302 투자조합출자금·1303 매도가능증권·2104 주임종단기차입금)이 `unclassified_accounts` 로 남아 `reconciliation_diff≠0` → `verified=false`. Fix (`ops/statements.py`): 미분류 계정에 한해 fallback rule — **1300번대 자산(prefix `13`, type=asset) → 투자활동** (1301/1302/1303 + 향후 1300번대 신계정 자동 포함), **차입금성 부채(`BORROWING_LIABILITIES`={2104,2105}, type=liability) → 재무활동**. ⚠️ 2101 미지급금/2102 미지급세금/2103 부가세예수금/2106 가수금 은 `OPERATING_LIABILITIES` 라 영업활동 유지 — 21xx prefix 로 싸잡지 않고 차입금 코드만 explicit set (미지급금 재무 누출 차단). 명시적 dict 멤버는 기존 로직대로 처리 (기존 분류·테스트 불변, 회귀 0). reconciliation 계산을 fallback 분류 後로 이동. 라이브 axec 2026-01~05 CF: diff −195M → **0, verified=true** (1302 −225M·1303 −200M 투자, 2104 +230M 재무). | 2026-06-04 |
| D-frame-2 | **evidence/source_file blob 은 항상 `.local/files` content-addressed 복사 + `storage_url` 은 portable (relative `<hash[:2]>/<hash>/<filename>`)** — `local://<host 경로>` 참조 금지 (컨테이너가 host 경로 read 불가 + source 파일 rename/move 시 깨짐), CWD 의존 absolute 경로도 금지 (host `/Users/.../​.local/files/...` vs 컨테이너 `/app/.local/files/...` 불일치). 근인: `ingest/entity_meta.py`·`ingest/resolution.py` 가 blob 을 복사 안 하고 `local://{pdf_path.resolve()}` 만 저장 → 외부 OneDrive/Downloads 포인터라 컨테이너에서 blob 부재; `_store_file_locally`·`ops/evidence.py`·`hometax.py` 는 복사는 했지만 `str(dest.resolve())` 라 CWD 의존. Fix: `_store_file_locally` 가 portable url 반환 + 신규 `resolve_storage_path()` 가 read 시 `settings.file_storage_path` 기준 absolute 로 매핑 (portable/`/app`/`/Users`/`local://` 4형식 tolerant — legacy row 무중단). 모든 write-site (entity_meta/resolution/hometax/ops.evidence) 가 `_store_file_locally` 경유. read-site = `http_server.py` storage-integrity probe 가 `resolve_storage_path` 사용. Alembic entity migration `0020_normalize_evidence_storage_url` 가 blob 이 `.local/files` 에 실존하는 기존 row (axec 158 + axev 19 = 177) 를 portable 로 정규화 (blob 부재 row 는 절대 미변경·notice; blob 절대 삭제 안 함). | 2026-06-04 |
| D-frame-1 | **pending_payroll: 분개 trigger = 실 거래 (matching principle)** — D-hive-26 의 즉시 분개 디자인 폐기. hive event → `pending_payroll` INSERT (예약). KB raw_tx → `match_pending_payroll` (date±3 + amount exact + name fuzzy) → 분개 자동 생성 + link. status: pending→matched/stale/cancelled. 회계 표준 (matching principle: 거래 발생 시점에 분개) 부합. 이중 분개 위험 0. | 2026-05-22 |
| D-matrix-1 | **Matrix: Rust + native MCP (no Python SDK)** — 첫 Rust 서비스. MCP Streamable HTTP 직접 구현 (JSON-RPC 2.0 + axum). 모니터링 서비스 저메모리·단일 바이너리 적합. D-bp-mcp-1 (Python FastMCP) 과 별개 precedent | 2026-05-23 |
| D-matrix-2 | **Matrix: WAN/인터넷 가용성 모니터링 + ISP 귀책 판별** — collector 에 `check_wan` (wan-gateway / wan-internet / wan-dns 프로브) 추가. gateway↑ + internet↓ ⇒ ISP/WAN fault 자동 태깅. 스키마 변경 0 (기존 `check_results` JSONB 재사용). 컨테이너 NET_RAW + iputils-ping, default interval 60→30 | 2026-06-03 |
| D-matrix-4 | **netheal — 호스트 인터넷 자가치유 데몬** (matrix 보완) — 인터넷 지속 끊김 시 DNS플러시→DHCP갱신→WiFi바운스 사다리 자동 실행. ISP 장애(gateway↑·외부↓)면 백오프(WiFi 안 건드림). root LaunchDaemon (컨테이너 밖, 호스트 en1 제어). 자동 재부팅 기본 OFF. `~/axe-netheal/` | 2026-06-04 |
| D-matrix-3 | **Matrix: backlog/roadmap/ship-log 구조화 SSOT** — backlog·roadmap·updates(ship log) 의 SSOT 를 hand-edited markdown → matrix-postgres(slug PK `B-foo`/`M1` 보존) 로 이전. matrix MCP 8 도구(`backlog_list/create/update/transition`·`roadmap_list/upsert`·`shiplog_append/list`)로 전 서비스 에이전트가 **멀티라이터-안전** read/write. docs 의 3 페이지는 **host-side 생성**(`axe ship docs` 가 matrix fetch→.mdx, matrix-git-push 모델 기각). known-gaps·decisions 는 narrative 라 markdown 유지. 동기 = git-markdown 멀티라이터 충돌(2026-06-03 세션 실측: known-gaps swept-commit·backlog 동시 +3). [ADR](https://github.com/axelabs-ai)=`matrix/docs/adr/backlog-roadmap-system.md`. **Phase 1(스키마 3테이블+MCP 8도구+import 파서) 구현·`cargo check`·import dry-run(206 backlog+16 ship+7 roadmap) 검증 완료, 미배포** | 2026-06-03 |
| D-hive-1 | Hive HR backend 신설 — frame 패턴 mirror (schema-per-entity, OAuth-RP) | 2026-05-21 |
| D-hive-21 | Payroll v2 — 한국 도메인 본질 7가지 (간이세액표 lookup · 비과세 분리 · 보수월액별 · 부양가족 · 정산 · 일할 · 보험별 라운딩). v1 단순 누진세 폐기. 첨부 급여대장 셀별 정확 일치. | 2026-05-21 |
| D-hive-22 | 급여명세서 메일 발송 인프라 (PDF + 평문 본문) + 법정 8항목 (근로기준법 §48 ②) | 2026-05-21 |
| D-hive-23 | M365 SMTP AUTH 의 deprecation 회피 → Graph API SendMail + client_credentials + DKIM/DMARC DNS (Cloudflare API token 자동화) | 2026-05-21 |
| D-hive-24 | 메시지 템플릿 관리 — shared default + entity override + Jinja2 SandboxedEnvironment + StrictUndefined + partial unique index | 2026-05-21 |
| D-hive-25 | Customize 강제 (`require_entity_override=True` — shared fallback 차단) + 발송·송금 audit log (`payslip_dispatch_log` + `payslip_send_log` with body_sha256) | 2026-05-21 |
| D-hive-26 | **Cross-service event consumer = `CONSUMER-worker` 별도 컨테이너** (consumer-grouped, multi-channel LISTEN). frame-worker 1개가 hive_events 받음 + 미래 magnet/stream 채널은 dispatch table 추가만 (컨테이너 추가 X). N×N 폭증 회피 (N+M, M≤N). in-process async task (P2) 검토 후 책임분리·격리·blue/green swap 무관·crash 격리 우선으로 sidecar (P1) 선택. | 2026-05-22 |
| D-hive-27 | **`employees.payroll_start_date` 단일 컬럼 분리** — `hire_date` 가 (i) 계약 발효일 (ii) 보수 개시일 (iii) 4대보험 가입일 3 의미 동시에 짊어지던 모순 해소. `payroll_start_date` Date nullable, NULL → hire_date fallback (정직원 90% 는 NULL → 자동). 임원계약 §2 (계약 시작) ≠ §6/§12 (보수·4대보험 시작) 케이스 대응. frame.executive_officer.appointed_date (= hive.hire_date) 정합 보존. 4대보험 가입일은 당분간 보수 개시일과 동일로 가정 — 다른 경우 발생 시 `social_insurance_enroll_date` 추가 검토. cycle.py 일할 base + employee_create/update MCP 옵셔널 입력 지원. alembic entity 0009. | 2026-05-29 |
| D-hive-28 | **`compute_period` 시그니처에서 `employee_ids` 제거** — D-hive-20 원작자가 정정 경로를 분리 (full re-run = `compute_period`, 사후 정정 = `compensation_events.adjustment`, D-hive-17) 했음에도 후행 adjustment leg 미구현 + `employee_ids` 파라미터의 동작 ("period 전체 wipe + 일부만 INSERT") 이 운영자에게 "부분 정정" 직관을 줘 다른 직원 payslip 손실 위험. 단순 결함 아닌 의도된 분기 + 미완성 + 의미 모호의 누적. 시그니처 모호 파라미터 제거가 원작자 의도 정합. 골든 테스트 + MCP wrapper 사용처 0건이라 호환성 영향 없음. 후속: `compensation_events.adjustment` 정식 MCP 도구 구현은 별도 backlog. | 2026-05-30 |
| D-hive-30 | **`compensation_plan_list` + `compensation_award_create` MCP 도구 신설 (M 트랙, D-hive-17 후행 leg)** — alembic 0004 의 plan/award/event 3-table schema 만 정의되고 등록 도구 미구현이라 운영자가 직접 SQL 박아야 했음. 본 결정에서: `compensation_plan_list(entity, plan_kind?)` (read) — plan_code 조회용. `compensation_award_create(entity, employee_no, plan_code, annual_amount_krw, effective_from, effective_until?, award_code?)` (admin) — employee/plan 존재 + active overlap 검증 후 INSERT (overlap 시 `COMPENSATION_AWARD_CONFLICT` raise, 옛 award `effective_until` 먼저 set 안내). `payroll_compute_period` 가 active salary award 없으면 skip — 본 도구가 페이슬립 산정의 선행. 사용 절차: plan_list → award_create → compute_period 재호출 → case A INSERT 자동 산정. 후속 (별도 backlog): award_update / award_close / plan_create / event_* 등. | 2026-06-05 |
| D-hive-29 | **`compute_period` UPSERT-with-gates (ABCDE) + `compensation_events.adjustment` 정공법 도구** — DELETE+INSERT 가 (i) `payslip_send_log` FK NO ACTION 과 충돌해 발송된 페이슬립 재산정 차단, (ii) ID 변경으로 외부 참조 dangling, (iii) audit history 가 두 ID 로 쪼개짐. UPDATE 는 PG WAL/MVCC 동등 또는 우위. 직원별 case A(없음→INSERT) / B(pending+send_log=0→UPDATE) / C(pending+발송됨→skip) / D(paid→skip) / E(reversed→skip) MECE 룰. UPDATE 는 산정 fields 덮어쓰기 / 운영 fields + 운영 메타 보존. 동반 구현 = `compensation_event_adjustment_create` MCP 도구 (admin scope) — payslips uq 제약으로 갈래 1 line append + 합계 보정 채택, `compensation_events.adjustment` row 가 audit 본질. `delta_krw` 부호 = `line_items.amount_krw` 부호. 발송된 페이슬립 정정 시 직원 메일 ≠ DB → 운영자 재발송 책임. D-hive-28 의 "DELETE+INSERT" 표현을 본 결정이 갱신, employee_ids 제거 자체는 유지. **보강 (룰 H, 동일 날짜)**: adjustment 도구는 case C 만 허용 — A→`PAYSLIP_NOT_FOUND` / B→`PAYSLIP_NOT_DISPATCHED` (compute_period 안내) / D→`PAYSLIP_ALREADY_PAID` (운영자 수동·다음달 소급 자동화 의도적 미구현) / E→`PAYSLIP_REVERSED` raise. 자동화 leg (frame 정정 분개 consumer / 정정명세서 자동 재발송 / 운영자 Teams 알림) 별도 backlog. **보강 (룰 I, 2026-06-05)**: NPS 4.5% → **4.75%** (2026 한국 국민연금 보험료율 9%→9.5% 인상, 1998년 이후 첫 조정). 사용자 원본 산식 `MIN(base, IF(base<370K,16650,IF(base>5.9M,265500,ROUNDDOWN(ROUNDDOWN(base,-3)*4.75%,-1))))` 그대로 적용 — base 천원 절사 + 양끝 정액 cap (16,650 / 265,500) + 외부 MIN wrapper. **룰 K (2026-06-05 최종)** — **근로소득세/주민세 base = R(과세표준) / 4대보험(NPS·NHIS·LTC·EI) base = 신고보수월액(`reported_income_*_krw`, NULL fallback R)**. 보수월액 컬럼 값은 R 과 같게 set 정책 (운영자 책임, 회계·공단 신고 정합). 정액 케이스는 R 기준과 결과 동일, 일할 케이스 (신규 입사·중도 퇴사) 에서 R(일할 변동) vs 보수월액(고정) 차이 가능 — 별도 backlog `B-hive-payroll-base-policy-decision` 정책 결정. axec.AXEC-002/003 reported_income 4,186,222/4,022,567 → 3,966,667 정합 UPDATE (axev 는 이미 정합). 골든 axec 4월 net 강태훈=3,607,277 / 한진우=3,571,577 — 사용자 expected 정확 일치. **MCP 노출 leg 정정**: D-hive-21 payroll v2 컬럼 8개 (reported_income_pension/health·dependents·children·vehicle·is_executive·meal/vehicle_allowance) 를 `employee_get` select / `employee_update` allowed 에 일괄 추가 — 운영자가 신고보수월액 조회·갱신 가능. 골든 강태훈/한진우 axec 4월 pension 188,370→198,830, net 3,605,007→3,594,547 (강태훈) / 3,569,307→3,558,847 (한진우). 5월 ps#7 강태훈 (case C) = adjustment 정공법 정정 필요, ps#8 한진우 (case B) = 다음 compute_period 호출 시 UPDATE 자동. | 2026-06-01 / 2026-06-05 |
| D-bp-mcp-calendar-2 | **Admin send-as 캘린더 쓰기** — `create_event` 등 6개 write tool 에 `as_user_email` optional 파라미터 추가 (find_free_time 제외 — 이미 attendeeEmails 로 다중 사용자 조회). 호출자 `role=admin` 이면 target user 의 캘린더에 직접 write 가능, event organizer = target user 표시. 구현: 신규 `getAppOnlyClient()` (graph.ts) = `msalApp.acquireTokenByClientCredential({ scopes: [".default"] })` 로 app-only token 발급 → `/users/{targetUpn}/events` Graph endpoint (UPN/email key — Blueprint `User.id` = Prisma cuid 이지 Entra oid 아님). Azure App **Application permission** `Calendars.ReadWrite` 필요 + tenant admin consent 1회. **권한은 Blueprint Next.js Azure App (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`) 에 부여** — MCP custom connector 의 별도 App (`482598f7-...`) 가 아님 (함정 [/ops/known-gaps#blueprint-azure-app-id-혼동](/ops/known-gaps#blueprint-azure-app-id-혼동)). 명령: `az ad app permission admin-consent --id 2b222356-1c36-48e0-96a3-2c5e0ecbf937` (Global Admin 계정 필요 — AXE 테넌트는 soohun.kang 만). consent 후 blueprint-app 컨테이너 1회 재시작 (MSAL `acquireTokenByClientCredential` 내부 토큰 캐시 비우기). 비-admin caller 의 `as_user_email` 사용 시 403 `send_as_forbidden`. D-bp-mcp-calendar-1 의 delegated 경로는 그대로 (caller 본인 캘린더 = 기본 동작). 동기 = 관리자 (`ai@axellc.com`) 가 직원 계정 (`soohun.kang@axellc.com`) 의 캘린더에 직접 일정 등록하는 사용자 시나리오. | 2026-05-26 |
| D-bp-mcp-mail-1 | **범용 이메일 발송 tool `send_mail`** — Blueprint MCP 의 첫 일반 outbound mail surface (기존 outbound 은 `create_event` .ics 초대뿐). caller 본인 mailbox 에서 임의 수신자(외부 도메인 포함)로 메일 발송. 구현: 신규 dispatcher route `POST /api/internal/mail` (calendar route 형제) → caller delegated token (`getClientForUser`; `Mail.Send` delegated 는 **이미** `src/lib/graph.ts` SCOPES 에 존재 → self 발송은 re-consent 불필요) → Graph `POST /me/sendMail`. params: `to`/`subject`/`body`(html\|text)/`cc`/`bcc`/`attachments`(Graph fileAttachment base64)/`save_to_sent_items`/`dry_run`. **Admin send-as** (`as_user_email`, calendar-2 와 동일 게이트): caller `role=admin` 이면 app-only token (`getAppOnlyClient`) + `/users/{targetUpn}/sendMail` 로 타 사용자 mailbox 발송. Azure App **Application permission** `Mail.Send` + admin consent 필요 — **Blueprint Next.js app (`2b222356-1c36-48e0-96a3-2c5e0ecbf937`) 측**, MCP connector app (`482598f7-...`) 아님 (함정 [/ops/known-gaps#blueprint-azure-app-id-혼동](/ops/known-gaps#blueprint-azure-app-id-혼동)). 비-admin 의 `as_user_email` → 403 `send_as_forbidden`. **Audit (injection-safety 정책 필수)**: 모든 발송 (`sent`/`dry_run`/`failed`) 이 append-only `MailSendLog` 1 row (caller · sentAs · to/cc/bcc count · subject · status · httpStatus · messageId) — Hive `payslip_send_log` + magnet decisions-ledger 패턴 정합. Graph `sendMail` 은 202 no-body → `message_id` null (Hive D-hive-23 동일; Sent 폴더 사본이 durable record). `dry_run=true` = 메시지 조립·검증·preview 만 반환, Graph 미호출. 동기 = 운영자 봇 (`ai@axellc.com`, role=admin) 이 외부 고객/임원에게 요약 메일 발송 필요. | 2026-05-29 |
| D-bp-mcp-1 | Blueprint MCP standalone (Python+FastMCP, frame/hive 1:1 미러) + 5층 양파껍질 회고 → 체크리스트 항구화 | 2026-05-21 |
| D-bp-mcp-2 | Blueprint MCP **blue/green pair** (frame `frame-mcp-blue/green` 패턴 미러, alias swap + Caddy reload, cloudflared 무중단) — single replica 가 build 시 5-10s 다운 유발하던 drift 해소 | 2026-05-21 |
| D-bp-mcp-teams-1 | **`get_teams_message` MCP tool** — Blueprint MCP 가 `teams.microsoft.com/l/message/...` deep link 를 본문·발신자·타임스탬프로 resolve. 구현 = Python MCP → docker `default` 망 → Blueprint Next.js `POST /api/internal/teams/fetch-message` (Bearer `BLUEPRINT_INTERNAL_API_KEY` 공유 비밀, `/api/internal/entity-roles` 패턴 미러) → 기존 `getChatMessage()` (bot identity `ai@axellc.com`, app perm `ChatMessage.Read.All`) 재사용. 동기 = Claude (web/desktop) connector 사용자가 Teams 링크 던졌을 때 "fetch 불가" 응답이 나오던 capability gap — connector 자체는 enable 됐으나 tool 이 없어서 호출 불가. **Scope**: contextType=chat (1:1·그룹) 만. channel-context URL 은 501 (`/teams/{teamId}/channels/{channelId}/messages/{messageId}` Graph 경로 + groupId 파싱 follow-up). bot 미멤버 chat 은 403 그대로 surface. | 2026-05-26 |
| D-bp-mcp-calendar-1 | **Blueprint MCP 의 첫 write surface = M365 caller 캘린더 CRUD** (7 tools: create/update/delete/get/list event, add_attendees, find_free_time). 사용자 요청 = Outlook calendar 자동 일정 등록 + 외부 도메인 (gmail/naver 등) attendee 초대. 구현: 단일 dispatcher route `POST /api/internal/calendar` (op selector — 7 ops in one file, calendar 가 1 logical CRUD domain). caller email → User.id → `getClientForUser(userId)` delegated Graph (`Calendars.ReadWrite`). 외부 attendee 도메인 = Graph 가 SMTP invite 자동 발송, 별도 provider 통합 불필요 (Google Calendar API 와 무관). NextAuth + graph.ts SCOPES 양쪽에 `Calendars.ReadWrite` 추가 — 기존 사용자 1회 re-consent (또는 admin tenant-wide consent). **D-bp-mcp-1 의 read-only 원칙과 무관**: D-bp-mcp-1 은 Blueprint 자체 도메인 (issue/session/workspace) 에 한정 — calendar 는 별개 도메인 (Microsoft Graph). 후속 [D-bp-mcp-calendar-2](#) 에서 admin send-as 추가. | 2026-05-26 |
| D-bp-mcp-teams-3 | **`reference` 첨부 파일 본문 fetch** — D-bp-mcp-teams-2 의 v2 가 `attachments[i].contentUrl` 을 surface 했지만 OneDrive/SharePoint 파일의 **실제 본문**은 별도 Graph 호출이 필요해서 호출자가 링크만 받고 끝남. 사용자 첫 메시지에 `.md` 첨부가 있어 즉시 노출된 gap. v3 = 신규 tool `get_teams_attachment_file(content_url)` + 신규 endpoint `POST /api/internal/teams/fetch-attachment-file`. `downloadReferenceFile()` (graph-client.ts:1507) 의 in-memory 버전 = `encodeShareUrl` (private helper 미러: `u!` + base64url, no padding) + `/shares/{enc}/driveItem/content` ARRAYBUFFER fetch. 응답 = 확장자 기반 3 분기: text-like (md/txt/csv/json/yaml/yml/log/xml/html/ts/py/sh/sql/conf/toml/diff 등) → UTF-8 decode 후 `kind: "text"`; image (png/jpg/gif/webp/svg) → base64 + `kind: "image"`; 나머지 → base64 + `kind: "binary"`. 50 MB 기본 / 100 MB hard cap (호출 시 `maxBytes` override 가능). 동기 = file vision 으로 가는 게 아니라 텍스트 파일이 압도적이므로 text → string 직답이 가장 큰 가치. 이미지 자료는 `get_teams_hosted_content` (inline message images) 가 별도 경로. | 2026-05-26 |
| D-bp-mcp-teams-2 | **첨부 + 인용 + 인라인이미지 지원 확장** — D-bp-mcp-teams-1 의 v1 응답이 `attachments[]` 를 drop 해 사용자 첫 smoke test 메시지에 첨부가 있어도 호출자가 알 수 없던 gap. v2 응답에 (a) `attachments[]` (`reference` OneDrive 파일 + `contentUrl` / `messageReference` Teams 인용 / Adaptive Card `content` JSON), (b) `quotes[]` (`messageReference` 자동 1-level resolve — `attachments.ts:resolveMessageReferences()` 패턴 미러, quote-of-quote 는 opaque), (c) `inlineImageIds[]` (`extractInlineImageRefs(bodyHtml)`) 추가. 신규 tool `get_teams_hosted_content(chat_id, message_id, hosted_content_id)` = Graph `/chats/{c}/messages/{m}/hostedContents/{h}/$value` ARRAYBUFFER fetch → 매직바이트 sniff (PNG/JPEG/GIF/WebP) → FastMCP `Image()` 반환 → Claude vision 직접 활용. 10 MB 상한. 신규 internal endpoint `POST /api/internal/teams/fetch-hosted-content`. | 2026-05-26 |
| D-bp-entity-1 | Blueprint 에 **entity 개념 도입** (axec/axev) — Workspace 에 `entityId` FK + `paraLayer` enum (PROJECT/AREA/RESOURCE/ARCHIVE). PARA 4 layer 모두 entity scope. hive 의 schema-per-entity 와 달리 row-level FK (Blueprint 의 cross-entity workflow 와 부합). [PR #339](https://github.com/soohunkang/blueprint/pull/339) + hotfix [PR #341](https://github.com/soohunkang/blueprint/pull/341) 적용 완료. | 2026-05-21 |
| D-bp-entity-2 | **PARA dispatch flow sub-consensus** — Project 종결 → Area/Resource 이관은 copy-with-provenance (`sourceWorkspaceId` / `sourceArtifactPath` / `copiedAt`). Archive = 시점형 archaeology, Area/Resource = 현재형 living knowledge (검색 분리). 자동 분배 = LLM 제안 + 사용자 확인. schema 부분은 D-bp-entity-1 PR 에 통합, UI 는 PR 5 예정 (Path B Spike 선행 권고). | 2026-05-21 |
| D-bp-para-1 | **PARA 범용 재구조화 — 조직론 모델 + dispatch "집(link)" + governance 분리 + 죽은 딜 본질** ([D-bp-entity-2](#d-bp-entity-2--para-dispatch-flow-sub-consensus) copy→link **revise**) — PARA=조직론(Area=영속기능/MCP=물질화 · Project=TFT · R/A는 Area 안). dispatch="집" 모델: artifact 몸통1·집N(**link 기본**), move=소모성→Archive→폐기, copy-curate=재사용본 저작→Resource (copy-with-provenance default 폐기). Blueprint=모든 Area 기본 substrate + 가로 결정로그, 서비스=졸업한 live 엔진. governance=결정로그(record, append-only) + 결재워크플로(process, layer), e-sign 통합=[B-bp-decision-pipeline-esign](/ops/backlog). **본질=죽은 딜 저장·harvest**. 신규코드=Rust. 상세=[/architecture/para-os](/architecture/para-os). | 2026-06-06 |
| D-bp-rust-1 | **Blueprint 점진 Rust 전환 (strangler-fig) — substrate = 첫 organ** ([D-bp-para-1](/ops/decisions) 열린질문 a/c/d 확정) — 백엔드를 도메인별 Rust(axum) organ 으로 점진 추출, Next.js=얇아지는 frontend-of-record (**프론트는 당분간 TS**, UI 전면 재작성은 별개·먼 결정). 첫 organ = **PARA substrate**(artifact/dispatch/knowledge-query). 경계=**Artifact-scoped**: substrate 가 artifact/artifact_link(+annotation)/mcp_schema 소유, `workspace_id`·`entity_id`=opaque 외부 ref(FK·중복 0), **Workspace+paraLayer SoR=Prisma 잔류**. 공존=**Postgres schema-split**(Prisma `public` / sqlx `substrate`, 한 blueprint-postgres, 교차쓰기 0) → [D-bp-artifact-4](/ops/decisions) "monolith first·신규 인프라 0" **refine(모순 아님): 인프라 동일·언어만 Rust·프로세스 분리**. 배포=blueprint compose sidecar `blueprint-substrate` (cloudflared/공개 MCP/blue-green/ops 온보딩 **없음**, `axe ship blueprint` 한 경로). Next→Rust 인증=기존 `BLUEPRINT_INTERNAL_API_KEY` Bearer. 모델=[D-bp-para-1](/ops/decisions) home/link("집") 1급 + **2-level scope(personal/shared) 확정**(a) + citation 8종([D-index-6](/ops/decisions) `index.*` · [D-gate-2](/ops/decisions) `gate.decision` 포함). Project staffing=후속 defer(c). 상세=ADR `docs/adr/blueprint-rust-migration.md` + [/architecture/para-os](/architecture/para-os). | 2026-06-06 |
| D-bp-entity-3 | **General bucket 본질 제거 + sys entity 도입** — `Workspace.entityId` nullable → **NOT NULL FK** (모든 workspace 가 정확히 1 entity 보유). ad-hoc / 시스템 row 는 seeded `sys` entity (kind=`system`, end-user dropdown 미노출). 사용자 결정 2026-05-22 ("General entity 는 필요하지 않습니다"). [PR #344](https://github.com/soohunkang/blueprint/pull/344) + hotfix [PR #345](https://github.com/soohunkang/blueprint/pull/345) (workspace.create 3곳 entityId 필수 — `_resolveEntityIdForCreate` helper: opts.entitySlug / drivePath guess / user.entityScopes[0]). 적용 후: axec 1 / axev 15 / sys 2 / null 0. | 2026-05-22 |
| D-bp-entity-4 | **TopNav 전역 EntitySelector + hive name sync** — 페이지별 entity dropdown 폐기 → TopNav 우측 단일 selector (URL `?entity=` primary + localStorage `axe:lastEntity` fallback). session.user.entityScopes 노출. ProjectsClient + ARAClient 가 URL query 읽기로 fetch 구성. Blueprint Entity.name 을 hive `shared.entity.legal_name` 한국어 정식 명칭 (액스코퍼레이션 주식회사 / 액스벤처스 주식회사) 으로 sync — 두 시스템 entity universe SOT 정합. [PR #346](https://github.com/soohunkang/blueprint/pull/346) + [PR #347](https://github.com/soohunkang/blueprint/pull/347) (Next.js 16 useSearchParams Suspense — /axe layout dynamic) + [PR #348](https://github.com/soohunkang/blueprint/pull/348) + [PR #349](https://github.com/soohunkang/blueprint/pull/349). | 2026-05-22 |
| D-bp-entity-5 | **Entity metadata 확장 + /api/entities + TopNav fetch** — Entity 에 `bizNo` / `fiscalYearStartMonth` / `countryCode` 추가 (hive shared.entity mirror, axec=366-86-03798 / axev=643-86-01377). `/api/entities` GET 엔드포인트 (서버 측 entityScopes 권한 검증). TopNav `ENTITY_LABELS` hardcode 제거 → /api/entities fetch — 새 entity 등록 시 client 재배포 0. seed drift detect 가 모든 mirror field 비교 (PR #350 의 early-out fix). [PR #350](https://github.com/soohunkang/blueprint/pull/350) + [PR #351](https://github.com/soohunkang/blueprint/pull/351). | 2026-05-22 |
| D-bp-entity-6 | **Team entity scope + Member.entityId rename** — `Member.entityId` (polymorphic User/Agent FK disambiguator) → `underlyingId`. D-bp-entity-1 의 `Workspace.entityId` (accounting entity) 와 naming 충돌 영구 해소. team-service 응답에 `entityScopes: string[] \| null` 추가 (human = User.entityScopes / agent = null). TeamClient + OrgChartView 가 chip 표시 (axec/axev = lime / sys = muted) + URL `?entity=` filter (human 만 적용, agent 는 system actor pass-through). [PR #352](https://github.com/soohunkang/blueprint/pull/352). | 2026-05-22 |
| D-bp-entity-7 | **User.defaultEntity — 사용자별 명시적 default entity** — 이전: TopNav 첫 로드 "all" / createWorkspace fallback "scopes[0]" 가 customers.yaml 순서 의존 (axec). 사용자별 default 명시 불가. 본 PR: `User.defaultEntity String?` 컬럼 + hydrateEntityScopesForUser 자동 set (null + non-empty scopes → scopes[0]; scope 외 default → scopes[0] fallback). session.user.defaultEntity 노출. TopNav fallback chain: URL → localStorage → defaultEntity → "all". createWorkspace fallback: opts → drivePath → defaultEntity → scopes[0] → throw. `/api/user/default-entity` PATCH 엔드포인트 (server-side gate). [PR #354](https://github.com/soohunkang/blueprint/pull/354). 운영 후속: Settings UI dropdown (endpoint 의 사용자 surface, 별도 PR). | 2026-05-22 |
| D-bp-entity-8 | **entityScopes YAML 순서 보존 + scopes[0] fallback** — D-bp-entity-7 의 후속 함정: hydrate 가 entityScopes 를 alphabetic sort 후 저장 → customers.yaml `["axev","axec"]` operator 의도 SOT 가 DB `["axec","axev"]` 로 reset → scopes[0] = "axec" → axev default 불가능. fix: hydrate 가 raw `resolved` 순서로 저장 (sort 는 set-equality 비교만). TopNav fallback chain 에 `userScopes[0]` 추가 — URL → localStorage → defaultEntity → scopes[0] → "all". session.defaultEntity null (build cache miss 등) 이어도 yaml 순서 가 운영 default. customers.yaml 의 user_entity_map list 순서가 진정한 SOT. 사용자 결정 "모두 default 는 axev" 만족. [PR #355](https://github.com/soohunkang/blueprint/pull/355). | 2026-05-22 |
| D-bp-entity-9 | **owned account switching (TopNav AccountSwitcher)** — Soohun 이 `soohun.kang@axellc.com` + `ai@axellc.com` + `cfo@axellc.com` 모두 운영. owner 가 owned AI agent identity 로 view-only switch. 본 PR: `User.ownedBy String?` self-FK (NULL 시 본인 계정, set 시 owner.id 참조). `POST /api/auth/switch-account { targetUserId }` 가 `target.ownedBy === caller.userId` 검증 후 httpOnly cookie `axe:active-user-id` set. `DELETE` 는 cookie clear (revert). session callback 이 cookie 발견 시 매 read 마다 ownership 재검증 → session.user.* 모두 target 로 override (email/name/role/entityScopes/defaultEntity). JWT 자체는 untouched — authenticated identity 의 audit trail 보존, view 만 변경. TopNav AccountSwitcher dropdown (owned ≥1 시만 노출, isImpersonating=true 시 lime border + tooltip). mid-session ownership revoke 시 자동 revert (defense-in-depth). admin role 도 bypass 불가 — owned-only. [PR #356](https://github.com/soohunkang/blueprint/pull/356) + hotfix [PR #357](https://github.com/soohunkang/blueprint/pull/357) (Suspense import). 적용 후 DB: ai@/cfo@ ownedBy = soohun.kang.id. | 2026-05-22 |
| D-bp-entity-10 | **AccountSwitcher 항상 노출 + Add account** — 이전: owned ≥1 (`accounts.length > 1`) 조건으로 single-identity 사용자에게 dropdown 가려짐 → 다른 계정 추가 진입로 부재. 본 PR: gate 를 `!activeUserId` 로만 (signed-in 모두 노출). dropdown 안에 `+ 다른 계정으로 로그인` 옵션 추가 → 선택 시 `signIn("azure-ad", ..., { prompt: "select_account" })` 호출, Microsoft 계정 picker 강제. PR #358 첫 시도는 `return null on accounts.length === 0` 함정 (initial state 가 빈 배열 → null mount → useEffect setAccounts 무효) → hotfix [PR #359](https://github.com/soohunkang/blueprint/pull/359) 가 condition 을 `!activeUserId` 로 좁힘. [PR #358](https://github.com/soohunkang/blueprint/pull/358) + #359. | 2026-05-22 |
| D-bp-entity-11 | **"전체 entity" 폐기 — 항상 specific scope** — 이전: TopNav 와 ProjectsClient/ARAClient/TeamClient 가 `entity === "all"` 분기로 전체 entity 합산 뷰 제공. 사용자 보고: "전체 entity 선택하는 것 잘 동작하지 않습니다" + 회계/거버넌스 측면에서도 cross-entity 합산은 의미 부재 (axec 와 axev 는 별 법인). 본 PR: ALL 상수 제거 + URL `entity` 가 userScopes 안 이 아니면 자동 specific 으로 rewrite (fallback: localStorage → defaultEntity → scopes[0]). 클라이언트 fetch 도 `entity` 항상 set. [PR #360](https://github.com/soohunkang/blueprint/pull/360) + follow-up [PR #361](https://github.com/soohunkang/blueprint/pull/361) (3 client 의 "all" 분기 제거 — ProjectsClient/ARAClient/TeamClient). | 2026-05-22 |
| D-bp-entity-12 | **user name = AccountSwitcher trigger (M365/Gmail 패턴)** — 이전: 사용자 이름은 정적 `<span>`, AccountSwitcher 는 별도 `<select>` — 사용자 보고 "nav 에서 id 클릭하면 다른 어카운트로 바뀌는거 구현 왜 안되어있습니까?". 본 PR: 두 element 통합 — 사용자 이름이 자체 dropdown trigger (button) 이고, 클릭 시 popover 메뉴 펼침 (owned accounts list + `+ 다른 계정으로 로그인`). click-outside / Escape close + active item lime / aria-haspopup="menu" + aria-checked. data-testid `nav-user-name` 유지 (E2E backward compat). isImpersonating=true 시 button border + label lime. `<select>` native 동작 (browser-provided dismiss / option 색) 잃지만 M365·Gmail UX 일관성 + click target = label 본질 우선. | 2026-05-22 |
| D-bp-entity-13 | **EntitySelector → button + popover (UI 일관성)** — 사용자 요청: "entity dropdown 도 id dropdown 처럼 보여지면 좋겠습니다". TopNav 의 두 dropdown (EntitySelector + AccountSwitcher) 중 EntitySelector 만 native `<select>` 로 남아있어 시각 불일치 + dark-mode option 색이 OS 마다 차이. 본 PR: 동일 button + popover 패턴 미러 (chevron rotate 120ms, click-outside / Escape close, aria-haspopup="menu", aria-checked). 각 행에 slug chip (axec/axev/sys) 표시 — 사용자가 legal name 만 보다가 slug 도 확인 가능. data-testid `nav-entity-selector` 유지 + `nav-entity-menu`/`nav-entity-item-{slug}` 추가. 부수: `userScopes` useMemo 로 wrap → react-hooks/exhaustive-deps 경고 0. | 2026-05-22 |
| D-bp-entity-14 | **`/axe/usage` entity scope (회계 분리)** — UsageLog → metadata.sessionId → Session.workspaceId → Workspace.entityId 경로로 axec/axev 비용 분리. K-IFRS/K-GAAP 회계 원칙 (거래는 발생 entity 기준 — economic locus = workspace). UsageLog 자체에 entityId column 추가 없이 derived join 으로 Phase 1 구현 — 228 row 규모에서는 query 부담 무시 가능, 5k+ scale 시 Phase 2 로 column + backfill. workspace 미존재 usage (ad-hoc Teams 1:1 / system) 는 `sys` entity 분류 → TopNav 에서 sys 선택 가능한 user (root) 에게만 노출. response 에 `entity` + `byEntity` aggregation 추가 (UI 가 각 entity 별 합계 표시 가능). `byUser/byChat/byWorkspace/byModel/byAuthMode/byDay` + recent 모두 entity filter 적용. UsageClient 가 URL `?entity=` 읽기 — TopNav EntitySelector 와 자동 연동. server-side `entityScopes.includes` 검증 (defense-in-depth). | 2026-05-22 |
| D-bp-entity-15 | **switch-account ownership = authenticated id (impersonated sibling switch fix)** — 사용자 보고: "id dropdown 에서 다른 id 들이 클릭 안되는 이슈". 근본 원인: `switch-account/route.ts` 의 `callerId = session.user.id` 가 effective (impersonated) id 였음. 시나리오: soohun 로 로그인 → ai@ 로 impersonate → cookie effective=ai@.id, JWT authenticated=soohun.id → ai@ 화면에서 cfo@ 클릭 시 `caller=ai@.id`, `target.ownedBy=soohun.id ≠ ai@.id` → 403. UI 의 try/catch 가 silent fail 하여 사용자에게 "클릭 안 됨" 으로만 보임. 본 PR: ownership 검증 + self-switch 모두 `authenticatedUserId` 기준. JWT 가 실제 switching authority 의 SOT (cookie 는 view override 만). 부수: UI `selectAccount` 의 silent fail 폐기 — non-ok response + network error 모두 `console.error` + `window.alert` 로 노출. 정책 명시: impersonated session 이라도 같은 authenticated owner 의 owned set 안에서는 자유롭게 sibling switch 가능 (ai@↔cfo@). 권한 상승은 여전히 차단 — owned identity 가 authenticated 가 아닌 target 으로 switch 불가. | 2026-05-22 |
| D-bp-entity-16 | **admin role 자동 sys entity 포함** — D-bp-entity-14 follow-up. `/axe/usage?entity=sys` 가 403 반환되던 함정. 근본: `hydrateEntityScopesForUser` 의 admin fallback 이 customers.yaml 미설정 시에만 작동 (`resolved.length === 0`). customers.yaml 에 명시된 user (axec/axev) 가 admin 이어도 sys 가 entityScopes 에 없음. 본 PR: admin role 이면 explicit list 뒤에 `sys` 자동 append. 정책: ad-hoc Teams 1:1 / system 사용량 회계 attribution 은 운영자만 확인 (회계 보고서 작성용). 일반 user 는 그대로 403 (자기 ad-hoc 보고 싶으면 admin 승격 필요). customers.yaml 손대지 않고 role 기반 자동화 — operator-editing-yaml-every-time 함정 회피. | 2026-05-22 |
| D-bp-entity-17 | **EntityRole 모델 — entity-scoped 3-tier 권한 (admin/owner/member)** — 트리거: axev 대표 강태훈 (`taehun.kang@axellc.com`) 이 axev HR/payroll 운영을 위해 admin scope 필요한데, 기존 `User.role=admin` 격상은 tenant-wide → axec 까지 영향하는 over-scope. 본 PR: `EntityRole { userId, entitySlug, role }` 모델 도입. role hierarchy `admin > owner > member`. tenant admin (Soohun, ai@) 은 모든 entity 에 implicit admin (admin fallback 정신 계승). hydration = sign-in 시 entityScopes 기반 member row 자동 생성 + slug 제거 시 revoke; UI grant 가 owner/admin overlay. Admin UI (`/axe/admin/users`) 에 entity 별 dropdown 추가 (tenant admin gate). 내부 API `GET /api/internal/entity-roles?email=...` (HMAC bearer) — hive/frame OIDC RP 가 PR 2/3 에서 호출해 자동 MCP scope grant 의 SOT. `entityScopes` JSON 은 dual-read (가시성 SOT) — 추후 별도 D 에서 폐기 평가. | 2026-05-22 |
| D-frame-fund-ksme-policy-check | **frame entity_versions/0003 의 `ck_policy_standard` 가 `fund_ksme` 미허용으로 fund entity 등록 좀비** — 트리거: 강태훈 대표 (2026-05-22 11:42) "frame 에서 `axe_ia_001` (액스 투자조합 1호, kind=kip) entity 오류없이 등록 및 활용할 수 있도록 조치" 요청. 근본: D-ops-30 (Migration #2) 가 fund kind 시 `shared.entity.accounting_standard='fund_ksme'` set 하는데, 0003 의 per-entity `accounting_policy.standard` CHECK 가 `('ksme','kgaap','kifrs')` 만 허용 → bootstrap INSERT 가 `COALESCE(accounting_standard,'ksme')` 로 `fund_ksme` 그대로 넣다가 CheckViolation → upgrade_entity 중단 → `axe_ia_001` schema 빈 채 (alembic_version 행도 없음, 0 tables) zombie. **Fix 2-prong**: (a) 0003 코드 패치 — CheckConstraint 정의 자체에 `fund_ksme` 추가 (신규 entity 가 처음부터 통과), (b) 0019 신규 migration (0018 fund_commitment_ledger 다음에 선형화) — `DROP CONSTRAINT IF EXISTS` + `IF EXISTS` 가드 + `ALTER ADD CONSTRAINT` 로 기존 entity (axec/axev/axep/axtest/axe_ia_001_diag) CHECK in-place 확장 + zombie schema 도 fresh 0001→head 로 진행 통과. 배포 후 강태훈 측 register_entity 재호출 → schema head + chart auto-apply 성공 경로 회복. | 2026-05-22 |
| D-frame-register-entity-atomic | **`register_entity` atomicity + mismatch detect — 강태훈 bug report 의 root cause #2/#3 잔여분** ([D-frame-fund-ksme-policy-check](/ops/decisions) 후속). bug report 의 (1) CHECK 제약은 PR #38 로 fix 됐지만 (2) explicit `accounting_standard='ksme'` 명시해도 stored 가 `fund_ksme` 이면 silent override + (3) `shared.entity` INSERT 와 `upgrade_entity` 가 별개 `begin()` 블록 → migrate 실패 시 좀비 row 잔존 — 두 잠재 함정이 미수정. 본 결정: `src/frame/db/migrations.py:register_entity` 를 2-prong 강화 — (a) **mismatch detect**: existing row 의 `accounting_standard`/`entity_kind`/`schema_name` 가 요청 인자와 다르면 ValueError (silent skip 금지, "use different entity_id or update via admin path"), (b) **compensating action**: `upgrade_entity` 실패 시 `inserted_now` 인 경우 `DROP SCHEMA CASCADE` + `DELETE shared.entity` 로 회수 (기존 row 는 보존). 6 신규 pytest (mismatch standard/kind/all-diffs · idempotent identical · zombie cleanup · pre-existing 보호) + 16 기존 PASS 무회귀. 본 audit 결과: bug fix 적용 후 `axe_ia_001` 은 valid empty fund 로 회복 (48 accounts seeded + accounting_policy fund_ksme bootstrapped + alembic 0019 head). 좀비 cleanup = `axe_ia_001_diag` (강태훈 진단용) + `bug_check_kip_20260524` (강태훈 fix 검증용) DROP. **axep** (액스파트너스 유한책임회사, 의도 불명, 데이터 0) 는 보존. | 2026-05-26 |
| D-hive-blueprint-entity-roles | **hive OIDC 가 Blueprint EntityRole 조회해 `admin` scope 자동 부여 (D-bp-entity-17 PR 2)** — 트리거: D-bp-entity-17 후속 + 강태훈 axev owner 격상 후 hive connector 의 admin 자동 활성. 이전: `auth_oidc.py` 가 OIDC 인증 시 무조건 `[read, write, approve]` 만 부여 (admin 은 운영자 HS256 manual). 본 PR: sign-in 마다 Blueprint 내부 API `GET /api/internal/entity-roles?email=...` (HMAC bearer `BLUEPRINT_INTERNAL_API_KEY`) 호출 → role `admin`/`owner` 면 그 entity 의 scope 에 `admin` 추가. **fail-open** (Blueprint 장애 시 default scope 유지 — lockout 회피), **5분 cache** (admin grant 가 Cowork 재인증 cadence 내 반영, Blueprint API 부담 최소화), `_log.info("admin scope granted on ...")` 로 audit. customers.yaml `services.hive.secrets[]` 에 `BLUEPRINT_INTERNAL_API_KEY` 추가 (Vaultwarden collection 동일). | 2026-05-22 |
| D-frame-blueprint-entity-roles | **frame OIDC 가 Blueprint EntityRole 조회해 `admin` scope 자동 부여 (D-bp-entity-17 PR 3)** — D-hive-blueprint-entity-roles 의 frame 측 1:1 미러. 동일 함수 시그니처/캐시 정책/fail-open. frame OIDC default `[read, write, close]` + role admin/owner 시 `admin` 추가 (`reopen` 은 여전히 HS256 only). customers.yaml `services.frame.secrets[]` 에 `BLUEPRINT_INTERNAL_API_KEY` 추가. | 2026-05-22 |
| D-dev-platform-1 | **Phase 1 contributor platform — axe-macmini multi-user dev access (B 안)** — 비전: macmini 10대 × customer 직원 1-2명/customer 가 시스템 개선 활동. 본 PR Phase 1 = 강태훈 + Soohun 양쪽 dev 진입 인프라. (1) macOS multi-user 정책 = first.last shortname (`soohun.kang`/`taehun.kang`), `dev` group + setgid + git `core.sharedRepository=group` 으로 `/Users/axe/{blueprint,frame,hive,axelabs-docs}` 공유. (2) sudoers `/etc/sudoers.d/ai-axe` 좁은 NOPASSWD (dseditgroup/sysadminctl/createhomedir/launchctl/sshd/install/tee `/Users/*`+`/etc/ssh/sshd_config*`/mv `/etc/ssh/sshd_config*`/chown). (3) SSH 강화 — `/etc/ssh/sshd_config.d/200-axe-hardening.conf` 에 PasswordAuth disable / PubkeyOnly / Root disable / ChallengeResponse disable (AllowUsers 는 자동화 user 명단 확정 후 별 round). (4) Cloudflare tunnel ingress + DNS `ssh.axe.axelabs.ai` (proxied, host SSH 22 로 forward) — public 외부 노출 없음, tunnel only. (5) 4 repo (blueprint/frame/hive — axelabs-ai org 으로 transfer 완료, axelabs-docs local-only) 에 CODEOWNERS skeleton (`* @soohunkang`) + PR template. macOS local password 는 Vaultwarden AXE org Default collection 의 `axe-macmini local: <user>` item 으로 등재 — Teams 채팅에 평문 송부 X. 본 PR 미완: Cloudflare Zero Trust App (Account-scope CF token 발급 후 별 round), branch protection (GitHub Team plan 또는 public 전환 결정 필요). | 2026-05-23 |
| D-dev-platform-2 | **`axe customers add <id>` CLI — Phase 2 customer onboarding 자동화 entry point** — Phase 1 의 강태훈 reference implementation 다음 step. AXE central side metadata 등재 (customers.yaml block 추가). Idempotent (이미 존재 시 거절), dry-run, validation (`id` `^[a-z][a-z0-9-]{1,30}$`, entity slug `^[a-z][a-z0-9_]{1,20}$` — underscore OK / hyphen X, frame/hive schema name compatibility), `--legal-name`/`--primary-domain`/`--tailscale-host`/`--public-domain`/`--entities`/`--yes`. 후속 (별 commands, 별 PRs): `axe customers vault-bootstrap <id>` (Vaultwarden collection), `axe customers github-team <id>` (GitHub team + collaborator slot), `axe customers dns-placeholder <id>` (Cloudflare CNAME stub), `axe customer deploy <id>` (customer macmini Docker stack). 첫 use case = realchoice (이미 customers.yaml 등재 — 다음 deploy 단계). | 2026-05-23 |
| D-dev-platform-3 | **`axe customers vault-bootstrap` + `github-team` CLI 추가 — D-dev-platform-2 follow-up** — `vault-bootstrap <id>`: AXE Vaultwarden org 에 3 collection 생성 (`<id>-macmini-local-accounts` / `frame-jwt-<id>-operators` / `<id>-deploy-secrets`). BW_SESSION 요구, idempotent. `github-team <id>`: `axelabs-ai/<id>-developers` team + 3 core repo (blueprint/frame/hive) push permission. member 추가는 `axe user add` (별 PR). 둘 다 dry-run + confirm prompt. realchoice 가 첫 use case (CLI dry-run 검증 완료). | 2026-05-23 |
| D-dev-platform-4 | **ai@axellc.com 통합 GitHub identity (`axe-labs-ai`) — git/gh 단일화** — 본 머신 git config (global) = `AXE Labs AI <ai@axellc.com>`, gh CLI active account = `axe-labs-ai` (PAT vault item `github PAT: ai@axellc.com`, Default collection). 모든 자동화 commit + push 가 단일 identity. 사용자 직원 (soohun.kang/taehun.kang macOS user) 은 자기 ~/.gitconfig 별도 (각자 본인 email/name). 기존 soohunkang gh account 도 keyring 잔존 — break-glass `gh auth switch -u soohunkang` 가능. **org 권한**: `axe-labs-ai` 가 axelabs-ai org **Owner** (team 생성 / collaborator add 자동화 가능). **PAT 발급 정책** (fine-grained): Resource owner = **axelabs-ai org** (본인 X — 본인 발급은 org repo 접근 X); All repositories; Permissions = Repository (Contents R+W / PR R+W / Workflows R+W / Administration R+W / Metadata R) + Organization (Members R+W / Administration R+W). PAT TTL 90일 (만료 2026-08-21), rotation backlog (`B-axe-pat-rotation-cron`). 2FA 권장 (`B-axe-labs-ai-2fa`). | 2026-05-23 |
| D-dev-platform-5 | **Cloudflare Zero Trust App + Free GitHub plan 결정 — D-dev-platform-1 미완 부분 마감** — Zero Trust: `ssh.axe.axelabs.ai` self-hosted App (`b903d8cd-...`) + Email domain policy (allow `*@axellc.com`, `e10f3198-...`). vault token `Cloudflare API - axelabs` (`fae8ff16-...`) 가 Account-scope Access Apps Edit 이미 보유 — 새 token 발급 불요 (notes 만 부정확했음). GitHub branch protection: Free plan 유지. 비용 0, invite-only collaborator + CODEOWNERS + PR template 으로 비전 충족 (강제 X — social norm + axe ship release-gate 가 main 직접 push blast radius 0). 향후 contributor 5+ 또는 외부 진입 시 Team plan / public 재평가. | 2026-05-23 |
| D-dev-platform-6 | **`axe vault unlock/lock/status` — Keychain-backed bw session, ai@ stdin tty 영구 회피** — 이전: BW_SESSION 자주 만료 + 운영자가 매번 `bw unlock --raw` 출력 채팅 회신 + transcript 노출 + ai@ subprocess 의 stdin tty 문제로 bw CLI 호출 fail. 본 PR: `axe vault unlock` 가 운영자 1회 interactive unlock 후 session token 을 macOS Keychain (`security add-generic-password -s axe.vault.session -a ai@axellc.com -w ... -U`) 에 push. _vault_env() 가 모든 ai@ subprocess 의 bw 호출 시 자동 fetch + `--session` flag 우회 패턴으로 stdin tty 문제 영구 회피. Keychain item 은 user 별 isolation (axe user 의 BW_SESSION 은 soohun.kang/taehun.kang user 접근 불가, macOS native ACL). `bw serve` 폐기 사유: UNIX socket / token auth 미지원 → TCP 8087 port 가 same-machine 다른 user 노출 risk. `cmd_customers_vault_bootstrap` 의 BW_SESSION env require 도 `_vault_env()` 자동 fetch 로 교체. | 2026-05-23 |
| D-host-setup-1 | **신설 `/ops/host-setup` page — macmini 13 layer SSOT** — 비전: 10 customer macmini 운영 + 신규 macmini 1 page reference 로 reproducible. 본 머신 (axe-macmini) 의 모든 layer (Hardware / macOS tunables / users + dev group / sudoers / SSH / Tailscale + Cloudflare / Entra / Vault / Docker stack / launchd / CLI / Git / OneDrive / Backup) 와 그 현재 값을 1 page 에 정리. 본 세션의 D-dev-platform-1~6 + 이전 세션의 D-ops/D-config/D-hive/D-frame 결정 다 cross-ref. 본 page 는 read-only reference + drift detect SOT — 변경 시 = 운영 실패 / 함정 발견 trigger. 자동화 CLI (`axe host bootstrap` / `axe host inventory`) backlog. | 2026-05-23 |
| D-hive-backup | **hive-postgres Tier A 백업 합산** — `axe-backup` 의 blueprint-postgres 블록 직후 `hive-postgres pg_dumpall` 블록 추가 (running check + size sanity + restic tag). Phase 1 (조직/휴가) + Phase 3 (payroll v2 axec/axev 실데이터) 운영 중인데 Tier A 0 이던 비대칭 해소. backup.mdx 시나리오 1c (hive DB 손상 복원) 신설. mysrt-postgres 는 SRT 외부 SOT 가능성 잔존 → 별도 결정 보류. | 2026-05-21 |
| D-docs-search-1 | **Nextra 4 UI 검색 = Pagefind 정적 인덱스** — 빌드 후 `pagefind --site .next/server/app --output-path public/_pagefind` 별도 실행. Nextra 4 가 v3 와 달리 검색 UI (`Search` 컴포넌트) 와 인덱스 생성을 분리해 두어 `next build` 만으로는 검색박스가 "Failed to load search index." 가 됨 (초기 scaffold 부터 줄곧 누락). `/api/search` JSON 엔드포인트는 LLM 용 자체 구현이라 무관 — UI 용 = Pagefind, LLM 용 = `/api/search`. | 2026-05-22 |
| D-bp-alert-1 | **L2 silent-drop alert = AXE infra only** (DB row + macOS launchd osascript). 외부 SaaS (Slack/Teams webhook) 모두 배제 — AXE 는 Teams 가 main comm 이고 Slack 미사용, 또한 Teams webhook 도 같은 Graph 종속이라 본 incident class 차단 불가. Channel 1 = `OperatorAlert` Prisma row + `/axe/admin/system` red banner (운영자가 dashboard 자주 보는 곳). Channel 2 = `[OPERATOR-ALERT]` console marker → host launchd `com.axe.operator-alert-notify` daemon 이 docker logs tail + osascript notification (Graph/Postgres 모두 무관, 운영자 mac 앞에 있을 때 즉시 push). 두 채널 독립 — DB 죽어도 marker 살아있고 vice versa. **본질·퀄리티·결과·안전** 4-axis 동시 만족. PR #340 의 Slack webhook design 폐기 후 본 결정. | 2026-05-22 |
| D-docs-roadmap-1 | **`/ops/roadmap` + `/ops/backlog` 분리, `/ops/known-gaps` 책임 좁힘** — 다른 세션이 들어왔을 때 무엇부터 할지가 5초 안에 보이도록 실행 큐 (backlog) 와 큰 그림 (roadmap) 을 별도 페이지로 분리. known-gaps 는 분석적 사실 기록만 유지 (entry point 아님). backlog lifecycle = 신규 / ready / in-progress / done. CLAUDE.md ritual 의 "known-gaps 단일 SoT" 를 "backlog = entry point + known-gaps = 함정/사실" 로 재정의. | 2026-05-22 |
| D-docs-updates-1 | **`/ops/updates` 신설로 4-페이지 시간축 분리** — `backlog` (현재) + `roadmap` (미래) + `updates` (과거) + `known-gaps` (사실). updates 는 3 섹션: (1) Ship Log = 매 ship 한 줄 (Phase 1 수동, Phase 2 자동 hook), (2) Highlights = 사람 큐레이션 narrative (직원·외부 IT), (3) API feed = `/api/changes` JSON (LLM agent fetch). build 시점 `scripts/generate-changes-json.mjs` 가 git log → `public/changes.json` export — runtime container 에 git 바이너리 없는 (.dockerignore 가 `.git` 제외) 함정 회피. CLAUDE.md ritual 도 4-페이지로 확장. | 2026-05-22 |
| D-docs-playbook | **playbook 표준 형식 박제 (`## AI 요청 프롬프트` + step block + 표준 꼬리 2줄)** — 행위 단위 페이지 (셋업/배포/공지/복구) 가 사람과 AI 양쪽의 단일 entry point. commit 18e99d8 (ssh-access 의 "맨 위 복붙 prompt block" 도입) 이 5/26 4 페이지 (ssh-access · vault-setup · claude-connectors · operator-broadcast) 로 정착 → 본 결정으로 [/architecture/playbooks](/architecture/playbooks) 박제 + 4 페이지 minor drift fix. 후속 라운드에 11 페이지 추가 playbook 화 (총 15) + frontmatter `playbook: true` + sidebar `⚡ ` prefix + `scripts/generate-playbook-catalog.mjs` (SSOT 표 자동 + `public/playbooks.json` deterministic export, prebuild 자동 hook + `npm run catalog` ad-hoc) 모두 본 결정의 확장. **2026-05-27 갱신**: 헤딩 `🤖 AI 에 던질 prompt (복사해서 본인 AI session 에 paste)` → `AI 요청 프롬프트` (이모지/괄호 제거, 한국어 명사구 통일). 15 페이지 + 본 SSOT 일괄 정합. 형식 drift = 사용자 mental model 재학습 비용 = 패턴 가치 훼손. | 2026-05-26 |
| D-docs-mdx-traps | **mdx 함정 3종 박제 + `scripts/verify-mdx-compile.mjs` prebuild 자동 audit** — playbook catalog 작업 라운드들에서 노출된 3 함정: (1) `## Heading {#explicit-anchor}` MDX 3 acorn parse fail (기존 함정, 운영자 commit `941196f`), (2) GFM 표 cell 의 backtick code 안 `\|` 가 column separator 로 잘못 인식 → `{` mid-cell unclosed → acorn fail (known-gaps line 386 의 `{reason: "a"\|"b"\|"c"}` 패턴 노출 — webpack cache 가 보통 가려두지만 catalog script 의 SSOT 갱신이 page-map invalidate → fresh re-parse 시 노출), (3) plain text 안 `<PascalCase>` 가 close tag 없는 JSX 컴포넌트로 인식 → build fail (decisions.mdx line 315 의 `Extension<AuthContext>` 노출). 모두 [/ops/known-gaps](/ops/known-gaps) mdx 함정 섹션에 등재 + `project_axelabs_docs_mdx_traps` memory 갱신. 미래 재발 예방 = `scripts/verify-mdx-compile.mjs` (mdx-js + remark-gfm + remark-frontmatter compile 시도, fail file 별 정확한 line/col 출력) 가 prebuild hook 자동 + `npm run verify:mdx` ad-hoc alias. `next build` 의 webpack 모듈 단위 알림보다 정확한 anchor 제공. | 2026-05-27 |
| D-bp-mcp-3 | **Blueprint MCP Postgres cutover 잔재 + startup probe** — D-config-17 (Postgres cutover, 5/15) 가 blueprint-app 만 다루고 `.env` 의 `DATABASE_URL="file:./data/blueprint.db"` SQLite legacy line 그대로 둠. blueprint-mcp 는 그 .env 받아 SQLAlchemy `ArgumentError: Could not parse SQLAlchemy URL` deep in ASGI handler → 모든 OAuth-bearing 요청 500 → Claude.ai UI 가 "Authorization with the MCP server failed" 로 표시 → 운영자가 secret/Azure config 만 의심 (실은 무관). MCP launch (5/21) 부터 종일 broken, container 8h+ healthy 였지만 200 응답 0건. **4-axis fix**: (1) **본질** — `.env` 의 stale `DATABASE_URL` line 제거 → compose default `${DATABASE_URL:-postgresql://blueprint:${BLUEPRINT_DB_PASSWORD}@postgres:5432/blueprint}` 자동 적용. (2) **퀄리티** — `config.get_database_url()` 가 `postgresql+asyncpg://`/`postgresql://`/`postgres://` 외 URL fail-fast (silent parse 대신 명확한 RuntimeError + remediation 안내). (3) **결과** — Claude.ai bearer → MCP 200, tools 실제 동작. (4) **안전** — Starlette lifespan 에 `SELECT 1` startup probe — DB 미접속/URL malformed 시 lifespan fail → healthcheck fail → `axe blueprint upgrade` blue/green swap 거부 (broken container active 못 됨). 양파껍질 6번째 layer = "Postgres cutover env_file 잔재 + SQLAlchemy silent parse fail" → mcp-server-checklist 에 항구화. | 2026-05-22 |
| D-bp-ui-5 | **Blueprint globals.css alias bridge 제거** — D-bp-ui-4 (PR #368) 가 모든 사용처를 새 토큰으로 마이그한 뒤 alias LHS 가 dead code. 본 PR 제거: `@layer base :root` 의 legacy var 9개 정의 (`--navy/--navy-light/--navy-mid/--white/--gray/--gray-light/--dark-border/--green/--red`) + `@layer utilities` 의 legacy class 5개 (`.bg-navy/.bg-navy-mid/.text-gray-ax/.text-gray-light-ax/.border-dark`). 보존: `--accent-glow` (41 tsx 사용, mode-invariant fixed RGB) + `.font-display/.text-accent` (새 토큰 직접 참조). 1 file, +7/-37. production deployed: `--accent-glow:#e3ff6626` 검증, legacy var 정의 0 hits. PR #370 — @axe/ui adoption 5-PR 시리즈 마무리. | 2026-05-22 |
| D-bp-ui-4 | **Blueprint legacy var() → @axe/ui 토큰명 일괄 마이그레이션** — 50+ tsx 의 `var(--navy)/--white/--gray/--gray-light/--dark-border/--green/--red/--navy-light/--navy-mid` 를 sed in-place 로 새 토큰 (`--bg-base/--text-primary/--text-muted/--text-tertiary/--border-subtle/--success/--danger/--gray-2/--gray-3`) 로 1:1 rename. 57 files, +1645/-1645 (mechanical, semantic change 0). globals.css 의 alias bridge LHS 는 보존 — deprecation 단계, 다음 PR (D-bp-ui-5) 에서 제거. Tailwind utility class 이름 (`.bg-navy`/`.text-gray-ax`/등) 그대로 (사용처 무수정), globals.css 의 정의가 새 토큰 직접 가리키게 자동 갱신. sed 순서 유의 — `-light`/`-mid` 가 base 보다 먼저. PR #368. | 2026-05-22 |
| D-bp-ui-3 | **Blueprint light/dark mode 토글 (cookie-backed SSR)** — D-bp-ui-1 토큰 인프라 위 첫 user-visible 기능. `next/headers` 의 `cookies()` (async) 를 RootLayout 에서 read → `<html data-theme={theme}>` 결정. cookie 없으면 dark 기본. `ThemeToggle` client 컴포넌트가 토글 시 (1) `document.cookie = 'theme=...; max-age=31536000; SameSite=Lax'` 1년 persistence, (2) `documentElement.setAttribute` 즉시 교체, (3) state 갱신. 다음 SSR 도 같은 cookie 읽으므로 FOUC/hydration mismatch 없음. next-themes 의존성 미추가 — 65-line custom. `/axe/settings` General 섹션의 placeholder GeneralRow Theme="Dark" 를 active 토글로 교체. `TopNav` 의 hard-coded `rgba(26,6,16,0.92)` → `color-mix(in srgb, var(--bg-base) 92%, transparent)` (modern color-mix Chrome 111+/Safari 16.4+/Firefox 113+). PR #367. | 2026-05-22 |
| D-bp-ui-2 | **Blueprint Space Grotesk inline → @axe/ui font 토큰** — D-bp-ui-1 후속 cleanup. 8 tsx 의 14 inline fontFamily refs 를 토큰화. SVG `<text fontFamily="'Space Grotesk', sans-serif">` × 8 (agents/org × 4, team/OrgChartView × 4) → `var(--font-sans)`. React inline style `fontFamily: "'Space Grotesk', monospace"` × 6 (system prompt textarea — fallback `monospace` 가 의도 신호) → `var(--font-mono)` (Sarasa Fixed K). layout.tsx 의 Space Grotesk `<link>` 제거 (의존성 0). 시각 변화: SVG = Pretendard 로 전환, textarea = Sarasa Fixed K mono 로 전환 (한글 박스 정렬 회복). PR #366. | 2026-05-22 |
| D-bp-ui-1 | **Blueprint `@axe/ui` 채택 + Tailwind 4 패치 2종** — Blueprint 가 `/Users/axe/axelabs/src/lib/` SSOT 를 `src/app/_axe-ui/` 로 sync. Blueprint 전용 패치: (a) dest path `src/app/_axe-ui/` (canonical `app/_axe-ui/` 와 다른 src/ source root), (b) `fonts.css` 의 `@import url(pretendard/d2coding)` strip — Tailwind 4 + Turbopack 이 토큰 파일을 globals.css 로 inline 한 뒤 위 @import 들이 다른 rule 뒤로 밀려 **CSS spec 위반으로 브라우저 silent drop** (Pretendard 미로드). 회피: sync 단계 strip + `layout.tsx <link rel="stylesheet">` (Clash Display 와 같은 패턴 확장). 35+ tsx 의 기존 `var(--navy)/var(--white)/var(--gray-ax)/--dark-border` 는 globals.css 에서 `var(--bg-base)/var(--text-primary)/var(--text-muted)/var(--border-subtle)` 로 alias — 비파괴 자동 전환. `<html data-theme="dark">` 기본, CSP `style-src`+`font-src` 에 `cdn.jsdelivr.net` 추가. `predev`/`prebuild`/`check-axe-ui` npm hook 으로 SSOT drift 감지. | 2026-05-22 |
| D-axe-ui-1 | **`@axe/ui` 단일 배포 채널 = git+ssh + axelabs.ai dogfood** — 디자인 시스템 (`/Users/axe/axelabs/`) 이 npm registry 없이 git+ssh 임포트 (`pnpm add 'git+ssh://...axelabs#tag'`) 만으로 모든 React 소비자 (Blueprint · frame admin · hive admin · docs.axelabs.ai MDX) 에 전파. axelabs.ai 회사 홈이 자기 자신을 dogfood — `axelabs.ai/ui` 라이브 쇼케이스가 깨지면 즉시 인지. Verdaccio / GitHub Packages / npm publish 모두 미사용 — 소비자 ≤3개 시점에서 부담만. v1.0 시점 재검토. Clash Display `<link>` 패턴은 Blueprint 와 일치 (Fontshare CSS @import 의 `f[]=` drop 함정 회피). | 2026-05-22 |
| D-bp-artifact-1 | **Blueprint artifact + PARA 지식 레이어 도입 — Blueprint 내부 meta-layer, MCPs 와 별 트랙** — Blueprint 의 typed fact unit (`Artifact`) 를 1st-class 도입. source 문서가 아니라 agent 가 직접 read 하는, 사전 추출된 사실 한 조각: per-field schema (versioned, `schema_id` FK) + per-field citation + confidence + audit_trail + paraLayer scope. MCPs (frame · hive · magnet · stream …) 는 domain-specific data system 으로 그대로 — Artifact 는 그 위에서 사실을 organize 하는 Blueprint 내부 meta-layer (별 트랙). **Schema authority = MCP service** — frame/hive/magnet/stream 등 각자 자기 도메인 schema 정의 + `/schemas` endpoint 로 노출, Blueprint 는 schema **registry 아니라 discovery / mirror** (fetch + cache + version). free-form 영역 (IC memo / Board / LP comm / Sector framework 등 MCP 권위 없는 영역) 은 **LLM 자율** (extract 시점 ad-hoc typed structure 또는 `freeform.llm@auto` content + citation wrap). 별도 system naming 거절 — "Blueprint 의 artifact + PARA structure" 라고만 부름. 5종 artifact 유형 예시 (`ICMemoArtifact` PROJECT · `BoardMeetingNotesArtifact` PROJECT→ARCHIVE · `LPCapitalCallArtifact` PROJECT · `SectorFrameworkArtifact` RESOURCE · `PortcoBoardKPIArtifact` AREA) — 이는 schema 미리 정의 아니라 LLM 자율 또는 IC/ic skill 의 own structure (frame.balance / hive.employee 같은 MCP authority 예시는 [/architecture/artifacts § Schema authority 분산](/architecture/artifacts#schema-authority-분산)). [D-bp-entity-1](/ops/decisions) 의 PARA 4 layer schema + [D-bp-entity-2](/ops/decisions) 의 workspace-level provenance 위에 **field-level** 로 진화. 상세 = [/architecture/artifacts](/architecture/artifacts). | 2026-05-23 |
| D-bp-artifact-2 | **Artifact 저장 = blueprint-postgres + JSONB content + JSONB citations** — Primary storage 는 Blueprint Postgres 16 ([D-config-16](/ops/decisions) cutover 완료) 의 신규 `Artifact` 테이블. 신규 인프라 0. 컬럼: `id` / `schema_id` (versioned schema FK) / `workspace_id` / `entity_id` (NOT NULL, [D-bp-entity-3](/ops/decisions) 정합) / `para_layer` (PROJECT/AREA/RESOURCE/ARCHIVE) / `content` (JSONB, schema 별 typed field) / `citations` (JSONB array) / `confidence` (numeric) / `audit_trail` (JSONB, propose/review/confirm/edit event 누적) / `parent_artifact_id` (PARA dispatch fork chain) / `created_at` / `updated_at` / `archived_at` (soft archive). Index: workspace / (entity, paraLayer) / schema / parent. content + audit_trail 무결성은 pg_jsonschema extension (확장 add-on) 으로 가드. 상세 schema = [/architecture/artifacts § 데이터 모델](/architecture/artifacts#데이터-모델). | 2026-05-23 |
| D-bp-artifact-3 | **Citation = 외부 source 의 stable ID** — Artifact 의 citation 은 **stable ID 만 보유, data 중복 0**. 6 kind: (1) `onedrive` (`drive_item_id` + `version` + `sheet`/`page`/`cell`), (2) `frame.*` (`entity` + `account`/`period` + `queried_at` — 회계 도메인 typed query snapshot), (3) `hive.*` (`entity` + `employee_id` + `queried_at` — HR 도메인), (4) `teams.message` (`chat_id` + `message_id`), (5) `mail.thread` (`thread_id` + `message_id`), (6) `external.web` (`url` + `fetched_at` snapshot 시점). large content / 원본 파일은 모두 원위치 유지 — OneDrive driveItem, frame DB resolution/journal IDs, hive DB employee IDs, Teams chat+msg, mail thread+msg, external API. artifact 가 frame `query_balance` 결과를 복사하지 않고 citation 으로만 가리킴 → resolver (`src/lib/artifact/citations/`) 가 fetch + cache. Trinity dashboard (Workspace × OneDrive × TeamsChat GUID 3축) 의 field-level 진화. | 2026-05-23 |
| D-bp-artifact-4 | **monolith first — 별도 KnowledgeStore service 분리 안 함** — Blueprint 가 이미 PARA OS home 이고 per-customer macmini isolation ([D1](/ops/decisions)) 안에서 Blueprint Postgres 동거가 자연. 추출 필요 시 나중. 대안 거절: **Graph DB (Neo4j)** — citation chain 이 graph 형태이긴 하나 query workload 가 hash-lookup + filter 위주, Postgres JSONB GIN 으로 충분 / **Vector DB (Qdrant) primary** — primary 로 두면 transactional consistency · FK · audit trail 다 잃음, semantic search 필요 시 pgvector extension / **ClickHouse** — analytical OLAP 강점은 있으나 artifact row count < 100k 수준의 transactional workload, column-store 이점 X / **MongoDB** — JSONB 동등 + sharding 미필요 + frame/hive Postgres 일관성, 별 DB 추가 부담 / **SQLite per-workspace** — multi-agent concurrent read-write 미적합. 확장 add-on (필요 시): `pgvector` (semantic search) · `pg_jsonschema` (JSONB validation) · `temporal_tables` 또는 `timestamptz` column (time-travel). 상세 비교 = [/architecture/artifacts § 저장 결정 + 대안 비교](/architecture/artifacts#저장-결정--대안-비교). | 2026-05-23 |
| D-bp-artifact-5 | **ctx skill 진화 — markdown PKM → artifact curation interface + markdown 점진 migration** — 기존 `ctx` (agent session delta → markdown append + parent KB propagation) 를 artifact store 의 curation interface 로 재정의. 대체 아님 — 진화. 흐름 4 단계: source → ingest → typed extraction (schema-aware LLM extract) → proposed fact (`audit_trail: { event: "propose", actor: "agent", ... }`) → ctx review (user confirm / edit / reject, UX 가 markdown diff + citation hover) → confirmed fact + audit (`audit_trail: { event: "confirm", actor: "user", ... }`) → agent reads via Query API (grep 안 함 — typed query + citation chain). markdown 은 보조 — review UX 의 diff 표면 + Archive 보존 + emergency cat (DB 장애 시). primary read path 는 Query API. 기존 `ctx sync` / `ctx review` / `ctx status` 3 mode 호환 유지 + artifact 모드 추가. **기존 markdown KB 는 점진 migration (항목별 AI-assisted ctx review)** — mass migration 안 함, 1 entry → LLM extract → proposed artifact → ctx review (사용자 confirm/edit/reject) → confirmed artifact + 원본 markdown entry `<!-- migrated: <artifact_id> -->` archive 마킹. 신규 fact 는 처음부터 artifact (markdown append 안 함). 5 단계 migration 흐름 상세 = [/architecture/artifacts § Markdown → artifact 점진 migration](/architecture/artifacts#markdown--artifact-점진-migration-ai-assisted). | 2026-05-23 |
| D-bp-artifact-6 | **Workspace lifecycle + Dispatch ≠ Close 분리** — Workspace.paraLayer 가 PROJECT / AREA / RESOURCE / ARCHIVE 4 layer transition 관리. (a) **Dispatch (anytime activity)** — Project 진행 중에도 artifact/file 을 다른 Workspace 로 흘려보냄. 2 mode: **copy** (새 artifact row, `parent_artifact_id` chain, source frozen) vs **link** (`artifact_link` 테이블 — multi-parent reference, 원본 1 row 만 유지). (b) **Close (한 번)** — `workspace.paraLayer` 한 번 변경 (PROJECT → ARCHIVE 보통, 또는 reclassify 다른 layer 로). (c) **Reclassify bidirectional 모두 허용** (ARCHIVE → AREA resurrect 포함, audit_trail 보존). (d) **Area cardinality = function 당 1개** — 중복 function Area 만들기 시 UI/LLM 가 "기존 Area 와 merge 권고". (e) AXEV 현재 Area 4 (**Finance / BOD / Legal / License (AC 관련)**), Resource = 0 (naturally emerge). (f) **OneDrive 폴더 mirror** — trinity-sync 가 2 시점 처리: dispatch 별 file 부분 이동 + close 시 workspace 전체 이동. (g) `artifact_link` 테이블 신설 (PK `(artifact_id, workspace_id)`, `link_type` ∈ `{'reference','sub_project','cross_link'}`, linked_at + linked_by). 상세 = [/architecture/artifacts § Workspace lifecycle](/architecture/artifacts#workspace-lifecycle). | 2026-05-23 |
| D-bp-artifact-7 | **LLM 호출 = Max plan OAuth 우선, Access = MCP tool 위주 (REST 최소화)** — 2 layer 정책. **Layer 1 (LLM 호출)**: artifact extraction / propose / review 의 LLM 호출은 **Claude Code OAuth token (Anthropic Max plan)** 우선. 비용 측면 per-token incremental 0 (Max plan 무제한 활용). Anthropic API direct (per-token billing) 은 **fallback / 대량 batch** 만 — 한 번에 1000+ extraction, vision-heavy ingest (pdf/pptx 대량 — 기존 `vision_ingest.py` 패턴 확장). Surface = Teams bot · 로컬 Claude Code CLI · 웹 (Max session) 모두 same OAuth. **Layer 2 (Access)**: artifact access 의 primary layer = **Blueprint MCP tool** (`query_knowledge` / `get_artifact` / `dispatch_artifact` 등). 별도 REST endpoint proliferation 회피 — 웹 UI 만 최소 REST (curation UX 용). 외부 agent 도 Blueprint MCP via OAuth-RP ([D-bp-mcp-1](/ops/decisions)). 권한 모델 single source ([D-bp-entity-17](/ops/decisions) EntityRole). 상세 = [/architecture/artifacts § LLM 호출 + Access 정책](/architecture/artifacts#llm-호출--access-정책). | 2026-05-23 |
| D-index-1 | **index 서비스 inception — AXE Labs 7번째 vertical, 투자 도메인 MCP backend** — 펀드 운용 lifecycle (소싱 → 실사 → IC → 포트폴리오 → 엑싯 → LP IR) 의 typed fact SoT. frame (회계) / hive (HR) 와 같은 layer. Blueprint 의 `ic` / `due-diligence` / `vc-deal-sourcing` / `portfolio-management` / `investor-relations` 5 skill 의 dataroom-bound run-time 산출물 (memo markdown / xlsx / yaml) 을 persistent typed artifact + cross-deal / cross-fund / time-travel query 가능한 형태로 승격. 상세 = [/services/index](/services/index). 5 skill 자체는 작성/조사 workflow 로 유지 — index 는 사실의 보존소. | 2026-05-27 |
| D-index-2 | **Rust + axum + sqlx + Day 1 artifact-first ([D-cortex-7](/ops/decisions) precedent)** — cortex 의 5-table reference 1:1 미러: artifact / citation / artifact_event / mcp_schema + owner_id RLS + `index_app` NOSUPERUSER role + `SET LOCAL index.actor` GUC. 사유 (D-cortex-7 4 사유 그대로): (a) artifact 의 discriminated union 과 Rust algebraic type 자연, (b) sqlx compile-time SQL check 가 schema/RLS query 안전성 보강, (c) MCP HTTP 서버 long-running 단일 process 의 panic-free 적합, (d) 단일 binary. Anthropic SDK 부재 → reqwest direct (`/messages` endpoint). MCP checklist 의 "Python+FastMCP 통일" 은 frame/hive/blueprint mcp 의 양파껍질 회고 기반인데 cortex/matrix 가 이미 break 한 precedent. 시작 = `cp -R /Users/axe/cortex /Users/axe/index` byte-by-byte. | 2026-05-27 |
| D-index-3 | **schema-per-entity Postgres 16 + 40xx 포트 범위** — frame/hive 패턴 1:1 미러. Shared schema (deal · target_company · fund_investment · sector_framework · lp_master · cross_journal_link · mcp_schema_version) + per-entity schema (financial_model · financial_driver · financial_scenario · financial_driver_value · financial_output · exit_matrix · dd_finding · ic_decision · portfolio_kpi_snapshot · risk_alert · valuation_snapshot · exit_signal · lp_communication · postmortem · audit_log). 펀드 entity (axe_ia_001 / axe_ia_002 / ...) 가 frame 의 `shared.entity` 와 정합 — frame 이 register 한 펀드를 index 가 그대로 사용. 포트: 4000 postgres / 4010 mcp blue / 4011 green / 4012 axe-index-proxy Caddy. cloudflared ingress `^/index(/.*)?$` → `host.docker.internal:4012`. 39xx (matrix) 다음 40xx 자연 할당. | 2026-05-27 |
| D-index-4 | **financial_model 6-table SoT + DSL formula + topo evaluator + xlsx round-trip** — 본 결정의 핵심. ic skill 의 `ic/finance/{3fs_*,scenario_deltas,exit_matrix,irr_analysis,irr_cell_ref,prob_tree,assumptions}` 산출물을 DB 1st-class entity 로 승격. 6 테이블: (1) `financial_model` (deal 별 1+, time_axis + horizon + locked_at + source_xlsx_sha256 + OneDrive citation), (2) `financial_driver` (code · kind=input/derived · formula DSL · dependencies[] · units · category), (3) `financial_scenario` (Upside/Base/Mgmt/Downside · probability · driver_changes narrative · parent_scenario_id fork chain — ic skill 의 PR-J 4-scenario 정합), (4) `financial_driver_value` (model × driver_code × scenario × period_index → value · is_override · PK 4-tuple), (5) `financial_output` (irr/moic/npv/exit_val/runway × scenario × period → value + inputs_hash sha256 reproducibility), (6) `exit_matrix_leaf` (scenario × timing × path × probability × proceeds_krw × axe_proceeds_krw + dead_leaf/concentration flag — ic skill `check_exit_matrix.py` 결과 캐시). DSL = Rust `pest` parser + 10 operators (`+ - * / min max if sum avg lag(driver,n) growth`) — xlsx formula 100% 가 아닌 IC memo cite 80~90% 만. Topo evaluator (`petgraph` topological sort) — driver DAG cycle detect + period 별 순차 평가. `compute_outputs(model_id, scenario_id)` 호출 시 driver_value snapshot sha256 → output row 의 `inputs_hash` 저장 → driver_value 변경 시 재산출 trigger. Round-trip: `ingest_financial_model_xlsx(deal_id, blob)` LLM-assisted (Anthropic Sonnet 으로 sheet/cell pattern → driver tree extract) + `export_xlsx(model_id, scenarios[])` 재생성. ic skill 의 4 sanity check (`xlsx_integrity` / `check_arithmetic` / `check_exit_matrix` / `citation_trace`) 가 `validate_financial_model` MCP tool 의 DB CHECK + trigger 로 흡수. 상세 = [/services/index/financial-model](/services/index/financial-model). | 2026-05-27 |
| D-index-5 | **fund_investment(deal × fund × round) — 3차원 N:M** — 본 결정의 두 번째 핵심. **deal 은 회사 단위 thesis, fund_investment 는 cash flow + IRR carrier**. 한 deal 이 (a) 여러 펀드에서 동시 투자 (Iippo: 액스 투자조합 1호 99,998,912 + 2호 100,000,000 = 199,998,912원, RCPS), (b) 다른 시점 follow-on round (Canopy: 1호 BW 5억 + v5 신주 follow 1억), (c) 다른 instrument (RCPS / BW / SAFE / 신주 / 보통주) 로 들어갈 수 있음. `fund_investment` row = (deal_id, fund_entity_id, round_label, instrument, committed_krw, invested_krw, share_count, price_per_share, entry_date, paid_date, pre_money_krw, post_money_krw, ownership_pct_post, ownership_pct_fd, status [committed/paid/locked_in/written_off/exited], conditions[] JSONB [Iippo 2호 의 6 PCC 같은 trigger]). **financial_model 은 deal 레벨** (회사 projection 은 fund 무관 동일) — `financial_output.axe_proceeds` 산출 → `fund_investment` 의 ratio 로 split → 펀드별 IRR = `irr(committed_at_paid_date, axe_proceeds × fund.ratio_in_round)` 독립 산출. 사용자가 강조한 "Sentry 1호/2호 IRR 각각 관리" 의 본질이 `fund_investment` row 분리로 자연 해결. 실 데이터 검증: Iippo 가 1:N (1 deal × 2 fund) 의 cleanest 케이스. 상세 = [/services/index/financial-model § fund_investment](/services/index/financial-model#fund_investment). | 2026-05-27 |
| D-index-6 | **Blueprint citation kind 7번째 `index.*` 추가 + Workspace ↔ deal N:1** — Blueprint artifact 의 citation kind ([D-bp-artifact-3](/ops/decisions) 의 6 kind: onedrive · frame.* · hive.* · teams.message · mail.thread · external.web) 에 **7번째: `index.*`** (deal · fund_investment · dd_finding · ic_decision · portfolio_kpi · financial_model · financial_output · valuation · lp_comm · risk_alert · exit_signal · postmortem · sector_framework · target_company). 예: IC memo 한 줄 "base case IRR 23.4%" 의 citation = `{ kind: "index.financial_output", model_id, scenario_id: "base", output_code: "irr", computed_at }`. xlsx cell ref 가 아닌 **DB cell ref** 가 primary citation unit. **Workspace ↔ deal = N:1** — Blueprint workspace 는 sector deep-dive / pipeline / fund container, deal 은 그 안의 artifact subject. ic skill 의 현재 "workspace.name = deal name" 가정 폐기 — deal-name-as-tag 로 진화. ic skill 폴더 규약 `ic/memo/v{N}.md` → `ic/deal/<deal_id>/memo/v{N}.md` 로 재구성 (workspace 단위 → deal 단위 artifact 그룹). 한 deal 의 IC convergence 후 별도 workspace spin-off 가 자연스러우면 link mode dispatch ([D-bp-artifact-6](/ops/decisions)) 로 cover. | 2026-05-27 |
| D-index-7 | **`/index/schemas` 권위 + 14 schema set 노출** — frame `/frame/schemas` 13 schemas + hive `/hive/schemas` 15 schemas 패턴 1:1 미러. SoT = `src/index/mcp/schemas.rs` (Rust 측, cortex 의 schemas.rs 미러). 첫 노출 14 schemas: `index.deal@1.0` · `index.target_company@1.0` · `index.fund_investment@1.0` · `index.dd_finding@1.0` · `index.ic_decision@1.0` · `index.portfolio_kpi@1.0` · `index.risk_alert@1.0` · `index.valuation@1.0` · `index.lp_comm@1.0` · `index.postmortem@1.0` · `index.sector_framework@1.0` · `index.financial_model@1.0` · `index.financial_output@1.0` · `index.exit_matrix_leaf@1.0`. Blueprint 의 [B-bp-artifact-schema-discovery](/ops/backlog) fetcher 에 `index` entry 1 줄 추가만으로 자동 mirror. 인증 required (JWTAuthMiddleware, frame 동일), versioning `@{version}` suffix, envelope `{ version, service, schemas }`. 상세 schema spec = [/services/index/schema-catalog](/services/index/schema-catalog). | 2026-05-27 |
| D-index-8 | **5 skill → service 진화 + `--push-to-index` mode** — `ic` / `due-diligence` / `vc-deal-sourcing` / `portfolio-management` / `investor-relations` 5 skill 의 진화 경로. **skill = workflow (LLM-heavy authoring), index = persistence (typed fact)**. (1) `vc-deal-sourcing` pipeline 표 → `index.deal` + `deal_stage_history` (영구 + cross-fund pipeline view), (2) `due-diligence` 체크리스트 → `index.dd_finding` + citation chain (finding 한 줄마다 출처 trace), (3) `ic` skill 의 18-section memo + 19-agent orchestration → run-time orchestration **유지** (LLM heavy 본질), 산출물은 `index.ic_decision` typed fact 로 push. memo PDF 본문 = OneDrive citation, scenario / IRR / postmortem cadence = typed field, (4) `portfolio-management` KPI 표 → `index.portfolio_kpi` time series (QoQ chart, cross-portfolio benchmark), (5) `investor-relations` LP 보고 → `index.lp_comm` audit + LP 별 last-contact view + AXE 펀드 성과 지표 자동 산출 (frame `lp_master` 결합). 새 mode = `/ic --push-to-index` — IC convergence (`/ic --finalize` 와 평행) 후 산출물의 typed fact 부분 (deal / ic_decision / financial_model / dd_finding / risk_alert / exit_matrix_leaf) 만 index 로 propose. ctx review queue 경유 후 confirmed → audit trail 시작. 기존 markdown / xlsx / yaml 산출물은 OneDrive 그대로 보존 (citation source). 상세 = [/services/index/skill-evolution](/services/index/skill-evolution). | 2026-05-27 |
| D-index-9 | **frame cross_journal_link mirror (펀드 회계 자동)** — index 의 `fund_investment` event (commit / paid / valuation change / exit) 가 frame 의 `commitment_ledger` (D-ops-22 펀드 회계 확장 — `[B-frame-fund-commitment-ledger](/ops/backlog)`) 와 자동 mirror 분개. 채널 = pg_notify (`index_events` 채널 from index-postgres → frame-postgres `LISTEN`, hive payroll → frame 패턴 1:1 미러). 이벤트: (a) `fund_investment.paid` → frame 의 capital call 분개 + commitment_ledger 차감, (b) `valuation_snapshot.recorded` → frame 의 평가손익 분개 (분기), (c) `fund_investment.exited` → frame 의 distribution 분개 + LP 비례 carry 산출. cross_journal_link 테이블 ([D-ops-22](/ops/decisions)) 가 양쪽 journal pair 무결성 보장. 이는 frame M2 (펀드 회계 확장) 의 자연스러운 client — index 가 frame 의 펀드 회계 trigger SoT. 단, M2 미완 시 index 는 stand-alone 가동 (event_outbox 보류 + 운영자 수동 review). | 2026-05-27 |
| D-index-10 | **3 deal validation fixture (Iippo / Sentry / Canopy) — Phase 0 acceptance bar** — Phase 0 의 schema correctness 검증 fixture. 3 deal 모두 ic skill 의 완전한 산출물 보유 (memo v4~v8 / scenario_deltas yaml / exit_matrix yaml / 3fs xlsx / irr_analysis xlsx / assumptions md). **Iippo** (킵코퍼레이션, AXE 199,998,912원 = 1호 99,998,912 + 2호 100,000,000, 1:N pure case) + **Sentry** (센티넬딥액티브, 1호 단독 5억 + 후속 5억 pro-rata option, 1:1 + future option case) + **Canopy** (사이클로이드, 1호 BW 5억 + v5 신주 follow 1억, multi-round 1:1×2 case) 가 fund_investment 의 3차원 N:M 을 모두 cover. Phase 0 acceptance bar: (a) 3 deal 모두 seed yaml → index DB ingest 성공, (b) ic skill 의 기존 `irr_analysis xlsx` 산출 IRR 과 index `compute_outputs(model_id, scenario_id)` 결과가 ±1pp 일치, (c) Iippo 1호 / 2호 IRR 각각 산출 가능 (별도 row), (d) Blueprint artifact 의 ICMemo citation = `{ kind: "index.financial_output", ... }` 로 정확 resolve. seed yaml SoT = `/Users/axe/index/seeds/{iippo,sentry,canopy}.yaml` (본 결정 PR 의 산출물). | 2026-05-27 |
| D-index-11 | **ic skill `--push-to-index` = 5th mode (FINALIZE 평행 track), atomic propose** — ic skill 의 기존 4 mode (INITIAL / REVISION / APPEND / FINALIZE) 에 5번째 mode 가 sequential 추가 아닌 **parallel track**. FINALIZE 는 `ic/final/` lock + PDF deliverable (OneDrive deliverable lifecycle), `--push-to-index` 는 typed fact persistence (DB lifecycle) — 둘이 같은 commit 에 묶이면 partial failure 시 rollback 모호. **Step 5.5** 신규 삽입 (gate_memo.sh PASS → humanize Stage 4 → pdf_quality 다음, postmortem_stub.py 자동 호출 이전): `--push-to-index` 플래그 detect → atomic `propose_deal_closure(deal_id, version, idempotency_key, payload)` 단일 batch MCP call → index 측 1 transaction 으로 모두 INSERT or all-rollback. **개별 propose tool 금지** (financial_model 따로, ic_decision 따로 → 중간 실패 시 inconsistent state). 산출물: ic/index_push_state/v(N).json (model_id, status, timestamp, errors 배열 포함) local checkpoint — retry-safe. 변경 범위 = SKILL.md (40-60 lines, Rule 25 + Step 5.5) + gate_memo.sh (80-120 lines post-V_COUNT) + orchestration-initial.md (50-80 lines) + build_3fs.py + calc_irr.py + postmortem_stub.py (~30-50 lines). 총 ~280-350 lines, **backward compat 100%** (flag 미사용 시 기존 동작). REVISION mode 와의 상호작용 = v(N+1) push 시 v(N) model 자동 supersede (last-write-wins, audit_trail 가 prior 보존). | 2026-05-27 |
| D-index-12 | **pmc (Post-Money Care) skill 신규 + portfolio-management / investor-relations 통합 3-phase deprecation** — IC 결의 ~ Exit/WD (5년+) 수명의 포트폴리오 케어 단계 통합 도구. 기존 2 prompt-only skill (`portfolio-management` 60 lines + `investor-relations`) 가 **개념은 정확하지만 실행 불가** (KPI 정의표만, 추출/저장/푸시 워크플로 없음) — pmc 가 둘을 흡수해 실행 가능 도구화. ic skill 의 19-agent 보다 lighter (8 sub-agent: data-fetcher / kpi-extractor / risk-alerter / valuation-updater / exit-signal-analyzer / board-pack-drafter / lp-comm-drafter / index-payload-composer). Trigger 3-tier: SCHEDULED (분기 첫 영업일 09:00 KST launchd) + EVENT (red alert 자동 fire — runway 6M 미만 / 핵심 인력 이탈 / burn_spike 30 percent 초과 / growth_slowdown / customer_concentration) + MANUAL (`/pmc --postmortem-fill` 또는 `--board-pack` 또는 `--push-to-index`). 폴더 = deal 폴더 안 `pmc/` 하위에 board_packs · kpi (snapshots + time_series) · risk/alerts · valuation/quarterly_nav · exit_signals · lp_comms · postmortem · index_push · cross_deal · manifests 10 sub-dir. **Postmortem 2-track 분리**: ic skill 의 `postmortem_stub.py` + `postmortem_reminder.py` 는 **유지** (gate_memo.sh PASS 시점 stub 생성 + launchd daily reminder = investment phase 책임), pmc 는 **fill + render + push** (`scripts/render_postmortem.py` 이관 + index.postmortem propose = portfolio care phase 책임). `references/postmortem-cadence.md` 는 양쪽 symlink (single SoT). **3-phase 마이그레이션** ([D-bp-artifact-5](/ops/decisions) 점진 패턴): Phase 0 (즉시) — pmc SKILL.md skeleton 등록 + portfolio-management/investor-relations 에 deprecation notice / Phase 1 (M7 Phase 1 launch 후) — 첫 quarterly cycle 실행 + KPI catalog → `pmc/references/kpi-catalog.md` copy + IR lifecycle → `pmc/references/ir-lifecycle.md` copy / Phase 2 (3개월 후) — 기존 skill SKILL.md soft-deprecate (Blueprint suggest 에서 pmc 로 redirect) / Phase 3 — 완전 archive. **5 risk_alert.kind enum 영구 freeze** (DB CHECK constraint): `runway_under_6m` / `key_personnel` / `growth_slowdown` / `burn_spike` / `customer_concentration` — portfolio-management SKILL.md 표 1:1 mirror, 새 kind 추가는 schema version bump 필요. | 2026-05-27 |
| D-index-13 | **Atomic `propose_deal_closure` MCP tool + idempotency key UUIDv5** — ic skill 의 `--push-to-index` 가 financial_model / ic_decision / fund_investments / dd_findings / exit_matrix_leaves / risk_alerts 를 개별 propose call (6+ round-trip) 시 partial failure risk: 첫 4개 commit 됐는데 5번째에서 schema validation fail → DB inconsistent state. 대신 **단일 batch MCP tool** `propose_deal_closure(deal_id, version, idempotency_key, payload)` 가 index 측에서 1 DB transaction 으로 묶음. 모두 INSERT or ROLLBACK (PostgreSQL serializable isolation). **Idempotency key** = `UUIDv5(namespace=deal_id, name="<version>::<content_sha256>::<actor_email>")` — ic skill gate_memo.sh 가 매 호출 동일 input 으로 동일 key 생성, DB UNIQUE constraint (`idempotency_record` 테이블, 24h TTL — hive `idempotency_record` 패턴 미러) 가 2nd call 을 409 + `context.prior_actor` 반환. **silent re-insert 절대 금지** — 두 ic session 이 동시 fire 시 race detect 됨. retry-safe: same input → same key → cached response. 본 tool 이 D-index-11 의 atomic Step 5.5 의 server-side counterpart. Phase 0 의 핵심 MCP tool (Phase 1 후속이 아닌). 비유: hive 의 `payslip_send_log` 의 `body_sha256` immutable 패턴 + frame 의 `idempotency_record` cross-entity dedup + post_journal 5계층 검증 의 atomic 정신 결합. | 2026-05-27 |
| D-index-14 | **3-layer error model + `IndexError` Rust enum SoT** — skill → index 모든 통신 에러는 일관 3-layer 표현. **L1 HTTP**: 401 Unauthorized + `WWW-Authenticate: Bearer resource_metadata="..."` (RFC 9728 정합, frame/hive 패턴). **L2 MCP body**: JSON envelope with `error.code` (영구 freeze enum, 예: `FINANCIAL_MODEL_CONFLICT`) + `error.message` + `error.context` (case 별 typed payload: prior_actor / prior_ts / idempotency_key 등) — silent fail 금지. **L3 skill handler**: ic skill 의 `lib/index_client.py` (또는 pmc 의 동형) 가 code 별 deterministic 분기 — `IDEMPOTENCY_CONFLICT` → 기존 표시 + retry 차단 / `VALIDATION_WARNING` → ctx review 사용자 confirm 요청 / `SCHEMA_NOT_FOUND` → upgrade hint / `SERVICE_UNAVAILABLE` → 로컬 drafts 보존 + retry 안내 / `CITATION_RESOLVE_FAILED` → cached value + warning chip. SoT = `src/index/error_model.rs` Rust enum (12+ case, deterministic mapping to McpErrorResponse) — silent fallback 금지. skill code 측 매칭 spec 은 본 enum 의 변경 시 deterministic 갱신 (test fixture 가 매 PR 검증). 양파껍질 ([D-bp-mcp-1](/ops/decisions)) layer 2 ("진단 불가, 모든 401 같은 메시지") 의 영구 차단. | 2026-05-27 |
| D-index-51 | **index-postgres 백업 (evidence_blob = 죽은 딜 SOLE 사본 보호) + teardown 역연산·감사 1급화 — frame reciprocal audit 마감** — [D-index-50](/ops/decisions) teardown 으로 10 딜(Sendy·apposter·dayton·DeveloperGroup·Eduon·JsEnl·Medistaff·Novachips·OpenResearch·Catalyst)의 OneDrive 원본 + 2 Blueprint workspace 삭제 → `evidence_blob`(328 blob/312MB)가 그 ~350 파일의 **유일 사본**. frame 이 index 의 Archive 설계를 reciprocal audit (A RLS gate · B dedup race · C size cap · D backup · E cross-deal refcount) → **2 FAIL 발견·마감**. **D (P0 — backup gap)**: index-postgres 가 `axe-backup` scope 에 부재 = SOLE 사본인데 백업 0 → 단일 실패점에서 영구 소실 가능. 수정 = `axe-backup` 에 index-postgres `pg_dumpall` 블록 추가 (매일 03:00 launchd · 312MB bytea → ~633MB hex dump → restic content-dedup 으로 ~274MB stored, snapshot `21735e04` 즉시 검증). **C (size cap)**: `register_evidence_blob` 에 512MiB 상한 가드 — bytea 1GiB hard-limit 아래에서 LOUD fail (초과 미디어는 inline 금지·citation 참조). **E (refcount + CAS 불변)**: cross-deal dedup blob 은 `deal_evidence→evidence_blob` FK(NO ACTION)가 참조 중 blob 삭제를 물리 차단 + teardown 이 index 행 미삭제로 안전. ⚠ 단 evidence_blob 의 [D-index-50](/ops/decisions) 마이그는 SELECT/INSERT only 를 명시 의도(주석 "UPDATE/DELETE grant 없음")했으나, 스키마 전역 `ALTER DEFAULT PRIVILEGES … GRANT … ON TABLES TO index_app`(20260526210000 artifact 스키마)가 *이후 생성된 모든 테이블*에 UPDATE/DELETE 를 자동 과다부여 → evidence_blob·teardown_event 도 열려 있었음(코드 INSERT-only 라 live 위반 0, by-construction 보장 아님; frame-audit E 의 "DELETE grant 부재" 기술은 당시 부정확) → **migration `20260606000004`(evidence_blob)·`20260606000005`(teardown_event) 로 UPDATE/DELETE 회수**, evidence_blob = append-only(INSERT/SELECT only) — content 불변 + index_app blob 삭제 불가가 by-construction. ★향후 orphan GC 는 별도 권한 role + refcount-guarded (`DELETE blob WHERE NOT EXISTS (SELECT 1 FROM deal_evidence …)`) 필수, per-deal 즉시삭제 금지. **reversibility (역연산 1급화)**: ① `export-evidence` CLI (evidence_blob → 파일 복원 + 디스크 재-hash 로 sha256 round-trip 검증, Sendy 28/28 0-mismatch) = teardown 이 되돌릴 수 있음을 증명. ② `teardown_event` audit 테이블 (deal·file_count·total_bytes·onedrive_path·blueprint_workspace_id·**sha_manifest** recovery map `[{path,sha256,kind,size}]`, RLS owner_only) = "무엇을 언제 어디서 떼어냈고 무엇으로 되돌리나" 의 단일 기록. **axe CLI 1급화**: `axe index teardown/restore/list-archive` — cross-service orchestrator (폴더해소 = Blueprint `Workspace.drivePath` 권위 → host index `ingest-evidence` archive → sha byte-for-byte verify → capture-first OneDrive rmtree + Blueprint workspace 삭제 → `teardown_event` insert). **경계** = axe CLI(orchestration·삭제·감사) · index binary(archive·restore·무결성). [D-index-50](/ops/decisions) `bin/deal_teardown.py` 스톱갭을 흡수. 108 test. | 2026-06-06 |
| D-index-50 | **Archive — 죽은 딜 evidence-blob 저장 + post-mortem schedule (Sendy 첫 완전 teardown)** — 2026-06-06 사용자 "Sendy 딜 저장·Resource화 후 종결 — OneDrive 폴더+Blueprint workspace 제거, 모든 파일 index 저장(frame evidence 참고), post-mortem 시점+할일". [D-index-49](/ops/decisions) dead-deal-harvest 의 §8 dispatch 실현: raw 파일=폐기가능·추출지식=영구, **폐기 전 흡수**. **gap**: index 가 citation(OneDrive 경로 포인터)만 보유 — 폴더 삭제 시 orphan. **구현 (2 subsystem, frame evidence 패턴 mirror·bytea 백엔드)**: ① `evidence_blob`(sha256 PK · content **bytea**=파일 bytes 자체 · content-addressed dedup, CAS RLS 없음) + `deal_evidence`(owner RLS 링크 · kind 7종 · original_filename · source_path provenance). bytea 선택 이유 = blue/green ephemeral 컨테이너 + 단일 DB 백업이 파일까지 커버(filesystem volume 불요·frame 과 차이). `get_evidence_blob` 은 deal_evidence(RLS) JOIN 접근통제. ② `postmortem_schedule`(deal · kind postmortem/revisit/follow_up · due_at · trigger_condition · action_items · status 상태머신) — PASS 결정의 disconfirming evidence 를 후일 actual 대비 검증. tool 6(register/list/get_evidence + schedule/list/update_postmortem, 29→35) + CLI `ingest-evidence`. **Sendy teardown**: 28 파일(6.6MB) 전부 흡수(dataroom 2 PDF+4 extract · finance 7 · ic_memo v2 · kb 6 · decision_log 8) + financial_model citation durable anchor(memo sha256) + post-mortem revisit(Q4 2026: sendyX 20+ customer·₩100M ARR / Series C ≤250억 / BEP). **검증(subagent, 삭제 전 게이트)**: 28/28 sha256 byte-for-byte(file↔store set-diff·in-DB content==hash·4.8MB PDF round-trip) · 의사결정 blob+typed fact 이중 보존(PASS사유·v1→v2 재calib·95.7%→35% IRR override 버그+버그파일 verbatim·post-mortem) · OneDrive 의존 0(self-contained) → SAFE TO DELETE. 108 test. ⭐ 부수: frame 에 bytea-vs-filesystem durability 통찰 전달(storage_url 가 백업·컨테이너 재빌드 survive 하나 점검). | 2026-06-06 |
| D-index-49 | **죽은 딜 harvest — why-passed/lost JUDGMENT 1급화 (`deal_judgment` artifact kind, dead-deal-harvest Pillar A)** — 2026-06-06 사용자 "죽은 딜 저장이 본질" (Blueprint PARA 철학 §11). live deal 관리는 moat 아님 — VC 지식의 압도적 대부분은 죽은 딜(시장맵·경쟁분석·섹터 thesis·valuation + 무엇보다 judgment "왜 패스했나")에 있고 보통 Archive 에 묻혀 증발 → 복리 corpus 로 harvest. **gap (recon)**: index 는 이미 passed/dead 를 *상태*로 1급 저장(deal.stage Passed/Writedown · 357 artifact)하나 judgment 층이 전부 finance-centric(왜 *숫자*가 그런가 — assumption/calibration). thesis-level **처분 JUDGMENT**("왜 passed/lost/died" + durable lesson, 예 Apposter "good company ≠ good deal"·"field DD kills thesis", Interstellar "high IRR ≠ Go")은 seed 주석·`fund_investment.conditions` 문자열에만 = typed·queryable 아님. **인식론 분리 (§0)**: claim(source-cited·live·갱신 = extracted_metric) vs **judgment(author·불변·supersede)** — `deal_judgment` 가 후자의 1급화. **구현 (D-index-45/46 기계장치 순수 확장, SQL migration 0 — store generic·kind/relation CHECK 없음)**: ① 새 artifact kind `deal_judgment`(payload deal_code/verdict/thesis/rationale[]/lesson/sector/tags[]/decided_by/at), FROZEN verdict `passed/lost/dead/invested/missed`, content-identity `deal_code·verdict`, para_layer=area. ② 새 FROZEN artifact_ref relation `supersedes`(judgment 불변 — 재판단=새 artifact 가 prior supersede). ③ seed `judgment:` block 에 `verdict:` 확장 (seed=SoT) → `seed-ingest --emit-judgment` 가 emit (`index_field(deal,"deal:disposition")` citation + 선택 corpus backing anchored_to, confirm-on-create, content-identity idempotent). ④ schema `index.deal_judgment` (/index/schemas active 14→15) + `verdict` frozen-enums 발행 → frozen_enums_hash 재-pin ([D-index-47](/ops/decisions) drift tripwire 정상 발화). ⑤ read tool `query_dispositions`(verdict/sector/tag 횡단)·`get_deal_judgment`(단일 deal) — 27→29 tool. ⑥ 23 canonical seed 전체 verdict backfill (헤더/conditions 에서 TRANSCRIBE, 4 병렬 agent + orchestrator QC, 날조 X) → **16 passed + 6 invested + 1 lost**. **검증 (additive-only 3중 증명)**: store 357→**380**(+23 deal_judgment), 재ingest 시 assumption/calibration/comp/base_rate 전부 +0(319 idempotent-skip), artifact_event op = propose 23 + confirm 23 only(edit/dispatch/archive 0), relational `deal.updated_at` 미전진(2026-05-30 고정), validate-seeds Δ=+0.00. 103 test, sqlx --check clean, blue/green 배포. **결과**: "왜 우리는 스마트링/교육/supplement 딜을 패스했나, durable lesson 은" 이 typed cross-deal 질의 + backing comp evidence 해소 가능 (Apposter→"Hardware wearable EV/Rev band" anchored_to). **B·C 설계 ratify (후속 backlog)**: B = sector harvest = artifact-first `sector` Resource (SoT `seeds/_sectors.yaml`, para_layer=resource, comp/base_rate/deal_judgment citation) + 휴면 빈 `sector_framework` 테이블 폐기 → [B-index-sector-harvest](/ops/backlog); C = cross-deal pattern (`query_sector`·`find_similar_deals` + **벡터 semantic** = hot sector Resource 색인, §5 "벡터=Resource 위 index, 가치 클 때만") → [B-index-cross-deal-pattern-query](/ops/backlog)·[B-index-sector-vector-index](/ops/backlog). **5축**: 본질(죽은 딜 judgment = 진짜 moat 의 1급화)·안전(additive·frozen-drift gate·fail-closed verdict·append-only)·결과(380 fact queryable judgment corpus)·혁신(claim/judgment 인식론 분리 + supersede 불변)·퀄리티(103 test·frozen verdict·transcribe-not-invent). | 2026-06-06 |
| D-index-48 | **브라우저 SSO 현황 페이지 — Blueprint OIDC 로그인 시 세션-게이트 firm-read 상세 열람** — 2026-06-04 사용자 "웹페이지에서 blueprint 기반 sso login 되면 상세 투자정보 조회 가능하도록". 현 `/index` 는 public aggregate(민감정보 0, leak-tripwire). **추가**: Blueprint OIDC **Authorization Code + PKCE(S256)** 웹 플로우 — `/index/login`(PKCE+state 서명쿠키→`/oauth/authorize` 리다이렉트, Entra 세션 재사용 = 진짜 SSO) · `/index/callback`(`/oauth/token` 교환→`auth::verify_blueprint_rs256` 재검증→email→세션쿠키) · `/index/logout`. 세션 있으면 `/index` 가 **firm-read 상세**(deal별 IRR(E[CF])·E[MoM]·P(loss) + 펀드 1·2·3호 성과 + calibration), 없으면 기존 aggregate + 로그인 버튼. **보안**: 세션·tx 쿠키 = HMAC(`INDEX_JWT_SECRET` 재사용, httpOnly+Secure+SameSite=Lax, 단명 exp) · state CSRF 가드 · 콜백 토큰은 RS256+iss+aud 재검증 · 무세션 경로 leak-tripwire 보존(공개 페이지 불변). **권한**: 세분화(IC allowlist)는 추후 — 우선 **AXE 인증 전체 firm-read**(superuser pool 전역 SELECT + 세션 게이트가 보호, RLS actor 불요). **Blueprint 의존(위임)**: `isAllowedRedirectUri`(platform-oidc.ts)가 현재 loopback+claude.ai 만 허용 → `https://axe.axelabs.ai/index/callback` 거부 → web redirect policy + `axe-index-web` static client 추가는 **병렬 IdP 세션([D-axe-idp-1](/ops/decisions))에 위임**(머지 전 web 로그인 비활성, index 코드 ready, env `BLUEPRINT_INDEX_CLIENT_ID`). index-only 구축. **✅ 2026-06-04 LIVE**: #383 merged(`19f30042`) + blueprint blue/green 배포(green active, edge 200) + index env flip(`INDEX_PUBLIC_BASE_URL`=https://axe.axelabs.ai + `INDEX_WEB_LOGIN_READY`=true) + recreate. authorize→Entra 수락 e2e 통과(운영자 첫 SSO 1회 잔여). 배포 시 3rd 세션 미커밋 WIP 는 stash 격리·복원(무손상, prod 미배포). | 2026-06-04 |
| D-index-47 | **skill ownership/distribution 아키텍처 ratify + P0 reconcile (index=SoT 거짓 해소)** — 2026-06-03 사용자 "위생 본질 전략 순" 의 *전략* leg (opus 서브에이전트). 직전 멀티에이전트 플랜 ratify: **index=truth(SoT bytes·semver·schema-contract·audit) · Blueprint=road(universal git→webhook→rsync 분배, 신규 transport 0) · surface=run(Agent SDK 로컬 디스크 읽기)**. **P0 "STOP THE LIE" 완료**(index commit `3b39a03`): `index=SoT` 가 `ingest` 에 거짓이었음(Blueprint base mirror 가 v2 구조추출로 AHEAD) → blueprint→index port 로 index 현행 superset 화(SKILL.md 285→348 · ingest.py 1443→2035 · test 신규, `diff -qr` 동일). ic byte-identical · pmc index-only 확인 · `INDEX_OWNED.txt` manifest + 죽은 launchd 폐기. **검증된 정정**: (a) 폐기 launchd 의 target script 는 실재(오늘 수정)·방향 안전(index MCP→blueprint skills-cache drift 검증, 파괴적 아님) — P2 binary(`index/bin/index-skill-sync.py`) 미존재로 비기능 → 폐기. (b) **⚠ 런타임은 아직 blueprint origin/main 서빙**: LIVE `com.axe.blueprint-skills-sync` launchd 가 blueprint→`~/.claude/skills` rsync — index=SoT 는 disk/git 에선 참이나 **런타임 반영은 P2(index→blueprint mirror+push) 필요**. (c) `owner:index` frontmatter 미기재(manifest 가 de-facto SoT) → follow-up. **잔여**: P1 stamp(provenance+schema-drift gate, index-side 安) · **P2 CI mirror(런타임 fix — blueprint write+push, production 영향)** · **P3 contamination purge(ic/dd/vc-sourcing/ir/pmgmt/legal 을 universal base 에서 제거 — 전 customer 컨테이너 영향, 高위험)**. P2/P3 = explicit go + blueprint PR 필요. | 2026-06-03 |
| D-index-46 | **seed = SoT for judgment facts — hardcoded backfill 폐기 (B-index-judgment-in-seeds ✅)** — 2026-06-03 사용자 "위생 본질 전략 순" 의 *본질* leg. index commit `8ec2c10`. [D-index-45](/ops/decisions) 가 318 judgment artifact 를 1회성 hardcode migration(`backfill.rs` 2221 LoC + `corpus.rs` 603 LoC)으로 넣은 것 = seed prose 의 중복 = transcription 부채. 제거: **seed.yaml 이 judgment 의 단일 SoT**. ① seed schema: per-seed `judgment:`(assumptions[]+calibrations[]) + 공유 `seeds/_corpus.yaml`(comps+base_rates), serde + fail-closed validate(identity-unique·FROZEN scrub_kind/relation·anchor XOR·band/prob). ② seed-ingest emit leg(`--emit-judgment`): relational ingest 후 judgment artifact 를 propose_judgment_fact 로 emit, comp/base-rate anchor 를 **natural name → corpus artifact_id** 해소, idempotent·confirm-on-create. ③ 23 seed + _corpus 마이그레이션(backfill.rs/corpus.rs 에서 verbatim) 후 두 모듈 **삭제**. **HARD GATE PASS**: (1) 전 23 seed 재ingest = **+0 new**(content-identity 일치). (2) fresh-reproduction — committed wipe(318→0, relational 무손상) + seed-ingest 단독 +318 재현, byte-identical, 유일 delta = **42 stale dup citation pruned**(corpus.rs 중복 = 제거 대상 부채; 587→545). (3) backfill 모듈 삭제, seed-ingest 유일 경로. (4) additive-safe(event propose/confirm only·append-only trigger intact·validate-seeds 23 Δ=+0.00). 정확 구성: **357 = 218 assumption + 58 calibration + 35 comp + 7 base_rate + 39 기존**. 51 test(6 invariant 를 backfill.rs→seed.rs 이전, 23 seed YAML=SoT 위에서 assert). **신규 deal 은 seed-ingest 로 자동 artifact화** — hardcode transcription 0. **5축**: 본질(seed=진짜 SoT)·안전(2중 증명+append-only intact)·결과(2824 LoC 삭제+자동화)·혁신(natural-name anchor 해소)·퀄리티(51 test+fail-closed validate). | 2026-06-03 |
| D-index-45 | **artifact-first judgment layer — 증거층↔판단층 결합 (Epic 1, 2-wave supervised)** — 2026-06-03 사용자 "Epic 1 같이 검토하고 들어갑시다. 실제 구현은 서브에이전트로" + "본질에 집중, quick fix 금지". index commit `1c707e1`. **휴면이던 artifact store(39 fact — Apposter 추출치뿐)를 relational compute substrate(23 deal)와 결합** — 모든 투자판단 fact 를 store 의 일급 typed+cited+audited artifact 로 승격. **설계 (검증으로 확정)**: ① side-bridge stringly-typed 테이블(quick-fix) 거부 ② relational 전면 재작성(안전 위반) 거부 → propose API 가 kind 에 generic 이고 `artifact.kind`·`citation.kind` 에 CHECK 없음 확인 → **migration 0**. 4 신규 kind: `assumption`(보정 가정)·`calibration`(surface↔calibrated delta)·`comp`(외부 비교)·`base_rate`(경험적 기저율), schema SoT `mcp::judgment_schema_defs` → `/index/schemas`(active 10→14)+`mcp_schema` upsert. FROZEN enum: `scrub_kind` 7(fat_tail_comp_scrub·genuine_wipeout·par_entry·dilution·over_pessimism_floor·ramp_haircut·multiple_anchor) + artifact_ref `relation` 4(calibrated_from·scrubbed_from·anchored_to·contradicts). **링크 = 일급 citation(side-table 아님)**: `index_field` ref `{deal,field}` → relational field_ref(예 `scenario:base:exit_multiple`·`output:irr_expected_cashflow`) + `artifact_ref` ref `{artifact_id,relation}`. `propose_judgment_fact` 가 `propose_extracted_fact` INSERT shape 재사용(multi-citation·content-idempotency·confirm-on-create·fail-closed guard). 2 read tool: `get_judgment_provenance(deal,field_ref)`·`list_calibrations(deal)`. **결과**: 23/23 deal → **357 fact**(assumption 218 + calibration 58 + comp 35 + base_rate 7 + 기존 39), 0 orphan judgment value. 모든 scenario 확률·exit multiple·recovery·ramp 가 backing assumption 보유 + AUDIT-TRAIL surface→calibrated delta 마다 calibration artifact 가 scrubbed_from comp 인용(예 Apposter MoM 8.60x→1.48x scrubbed_from Oura 10x PSR via fat_tail_comp_scrub). **안전(additive-only 3중 증명)**: artifact_event op = propose/confirm 만(edit/dispatch/archive/restore 0) · relational updated_at 가 seed-ingest(05-30) 이후 미전진 · validate-seeds Δ=+0.00. 48/48 test(backfill self-contract 포함) · sqlx --check clean. **과정**: wave1 = machinery + 14/23 — **adversarial gate 가 61% 미완 적발**(per_deal 일부 aspirational, 병렬 agent crate 충돌) → 운영자 직접 미실행 subcommand 실행 +4 → wave2 = 미작성 5딜(Medistaff·Nanora·Novachips·OpenResearch·Superman) author + **6 rival backfill module → 1 `backfill.rs` + `corpus.rs` 통합**. **5축**: 본질(artifact-first 실재화 = 판단 기억)·결과(357 fact queryable)·안전(additive 3중+gate)·혁신(scrub→comp 역추적 provenance·우리 epistemic 차별점)·퀄리티(48 test·frozen enum·0 dangling ref). **잔여(방향)**: judgment data 를 structured seed field 로 이전 → ingest 가 artifact 파생(현 1회성 transcription) [B-index-judgment-in-seeds] · Epic 2 confidence load-bearing · Epic 3 self-calibrating prediction→outcome loop. | 2026-06-03 |
| D-index-44 | **ingest 데이터품질 + lifecycle 안전 — metric_kind 세분 + 회귀 스위트 (5축 평가 후 병렬 2 unit)** — 2026-05-30 사용자 "지금 가능한 작업 서브에이전트 진행. 본질·퀄리티·결과·안전·혁신". [D-index-43](/ops/decisions) 잔여를 5축 평가 → 최고가치 2개 병렬(non-conflicting repo). **(X) metric_kind 세분** (blueprint PR #380 merged `931284b9` → `~/.claude/skills/ingest` webhook sync): reconcile 가 스스로 노출한 근본결함 — coarse `valuation` 이 절대 EV(270억) + EV/EBITDA 배수(9x) + 음수 helper(−74) 혼재 — 를 source 에서 fix. order-priority 분류기(`moic`→`multiple`→`valuation`) + per-kind curation: `valuation`(절대 EV/EqV/proceeds, currency fmt)·`multiple`(EV/EBITDA·EV/Rev·P/E·PSR·PBR·배수)·`moic` 독립. curation: multiple 은 양수 0~100x band·valuation 은 양수 절대값만 → 음수 EV/EBITDA helper drop. test 52/52, irr·moic·cagr **0 regression**(XIRR function-path 무영향). **(Y) lifecycle 회귀 스위트** (index `c8b3746`): 8 test — idempotency·1:1:1·status 파생·confirm/reject·append-only(grant+trigger 이중)·RLS 격리·reconcile·**seed-draft non-ingestability**(serde reject 로 증명). tx-rollback isolation + graceful skip, 35 pass, 0 residue, offline-safe. ⭐ **발견(버그 아님)**: append-only trigger 가 `ON DELETE CASCADE` 도 차단 → event 보유 artifact 는 normal session 삭제 불가 = **audit 영속성 보장**(operational invariant). **통합 결과**: 정제 classifier 재ingest → store refresh → artifact store split 반영: **valuation 12 + multiple 4**(이전 conflated 10) + irr 8·cagr 7·moic 4 + figure 4 = 39 facts. extractor 결함이 정량 데이터로 노출(reconcile)되고 source 에서 닫힘 — **self-correcting 루프**. **5축**: 본질(근본 데이터품질)·결과(split 가시)·안전(회귀 lock+audit 영속)·혁신(unit-aware 분류기+self-correcting reconcile)·퀄리티(52+35 test). | 2026-05-30 |
| D-index-43 | **artifact-first lifecycle 완성 — Query API + ctx review + reconcile + seed-draft scaffold ([D-index-42](/ops/decisions) 잔여 4 완결)** — 2026-05-30 사용자 "서브에이전트로 종결 후 보고". D-index-42(propose)가 남긴 4개를 2 unit 순차 구현(supervised subagents) + blue/green 배포. index commit `3eece93`(lifecycle)+`118568a`(seed-draft). **(1) Query API read path** — `query_artifacts`(para_anchor/kind/metric_kind/status 필터)·`get_artifact`(payload+citations+event history). status = 최신 `artifact_event.op` 파생(propose→proposed/confirm→confirmed/reject→rejected; edit·dispatch 는 직전 유지) — agent 가 grep 대신 typed query. **(2) ctx review** — `confirm_artifact`(op=confirm; edit_payload 시 payload UPDATE+before/after 감사)·`reject_artifact`(op=reject+reason, row 보존). proposed→confirmed/rejected. **(3) L3-lite reconcile** — `reconcile_artifacts`: metric_kind 별 value spread flag, 불일치 `{value, source, location, status}` 전량 surface (silently 택1 금지). Apposter irr spread 86%(Exit IRR 0.572 vs Min IRR 0.08) + 5 group 전부 flag. **(4) L2 본령 seed-draft** — `draft_seed_from_artifacts`: deal artifacts → **보수적 seed.yaml SCAFFOLD**(`index draft-seed` CLI + MCP). ⭐ **날조 금지를 구조로 강제**: surface metrics(57.2% 등)→AUDIT TRAIL 주석+citation only / scenario probability·calibrated scenario·wipeout·baseline·intake = 전부 `~ TODO` / valuation 후보 다수(270억 vs 349억)→자동선택 거부 / draft 는 의도적 non-ingestable(validate-seeds 가 `~` placeholder 에서 fail) — judgment gate 가 문서 아닌 타입시스템으로 강제. coverage(Apposter): auto_filled 0 / needs_human 18 / surface_metrics 29 — 사람은 skeleton·number-hunt 절약, calibration([D-index-24](/ops/decisions)/26/21) 판단은 보존. **전 tx RLS 정식경로**(set_current_user), build clean online+offline(sqlx guard), 27 test pass, 6 신규 MCP tool live(blue/green, ext healthz 200). → **artifact-first 전 lifecycle 가동**: propose→query→review→reconcile→draft. **잔여(축소)**: ctx review UI(Blueprint markdown-diff 표면, 현재 CLI/MCP only) · Query API 를 /ic·seed-building 의 primary read path 통합 · extractor metric_kind 세분(valuation 이 EV+EV/EBITDA 혼재 — reconcile 가 노출한 finding) · 전 deal artifact화(현재 Apposter 1건) · L4 versioned diff. | 2026-05-30 |
| D-index-42 | **ingest 고도화 L2(부분) — extraction → proposed artifacts (artifact화, artifact store 첫 가동)** — 2026-05-30 사용자 "ingest 때 artifact화 한다던 것 구현됐나?". [D-index-41](/ops/decisions) L1(extraction→JSON sidecar) 후속 — sidecar 의 fact 를 **index artifact store 에 proposed artifact 로 적재** ([D-bp-artifact-5](/ops/decisions) 흐름 "source→ingest→typed extraction→**proposed fact(audit_trail propose)+citation**" 의 전반부 실현). **발견된 gap**: index 의 artifact/citation/artifact_event store ([D-index-2](/ops/decisions) cortex mirror, Day-1 schema) 가 **0행 — 한 번도 안 쓰임**. 23-deal 은 relational deal-domain 테이블에만 존재, artifact-first 층은 dormant 였음 (정직히 미구현). **구현** (index commit `4ca5868`, blue/green 재배포 live): **`propose-from-ingest <dataroom>` CLI + `propose_artifacts_from_ingest` MCP tool** — `*_xlsx.cells.json` key_outputs + `*_pdf.figures.json` figures → 각 fact = `artifact`(kind=extracted_metric|extracted_figure, payload value+formula+metric_kind+location, confidence, para_layer=resource) + `citation`(xlsx_cell|pdf_figure, ref=cell/page 증거) + `artifact_event`(op=propose, actor=system:ingest). **RLS 정식 경로** (index_app NOSUPERUSER + index.actor GUC — `db::set_current_user` 첫 실사용자, superuser bypass 아님). **idempotent** ((owner,kind,source_file,location), 재실행 0 new). **⭐ before/after (Apposter)**: artifact store **0 → 35** (31 metric + 4 figure, 1:1:1 artifact:citation:event). Exit IRR 57.2% 가 이제 typed queryable artifact (`metric_kind=irr`, citation `Return!Y62`, propose event) — D-index-41 에서 surface 된 fact 가 이제 **store 적재·인용가능**. append-only+RLS live 검증 (타 actor 0행, event UPDATE/DELETE 거부). **잔여**: ctx review(confirm/reject) + Query API read path + L2 본령(seed/financial_model 초안 자동생성) + L3 cross-source reconciliation. | 2026-05-30 |
| D-index-41 | **ingest 고도화 L1 — extraction-not-transcription + source-type-aware structured extraction (재무모델·IR 정문 손실 해소)** — 2026-05-30 사용자 "ingest 역량 끝까지 고도화, 서브에이전트로, before/after 만". [B-index-ingest-structured-extraction](/ops/backlog) 의 **L1 ship** (설계 ratify 동반). **문제**: ingest 가 dataroom xlsx/pdf 를 md 로 photocopy 하던 게 lossy — xlsx 는 값만 표로, 수식은 40개 truncate 후 `<!-- formulas: +182 more -->` 주석에 매몰 → return model 의 IRR/MoM 기계추출 불가 (23-deal 적재 시 수작업 발굴해야 했던 근본 원인). 정문 손실은 ic·index 에서 복구 불가 (GIGO). **L1 구현** (blueprint PR #379 merged → `~/.claude/skills/ingest` v2, launchd webhook sync): **convert_xlsx v2** — `{stem}_xlsx.cells.json` sidecar (시트별 full grid: value+formula+number_format+label, named_ranges, **key_outputs[]**) + **key_outputs 자동탐지** (XIRR/IRR/MIRR/NPV/XNPV 함수 + IRR/MoM/NPV/CAGR/valuation/EV/배수/수익률/밸류 라벨 정규식; numeric-value + rate-타당성 게이트로 false-positive 차단) + md `## Model Summary` / `## Key Outputs (detected)` / per-sheet formula↔value 표(truncated 주석 대체) / `## Named Ranges`. **convert_pdf v2** — `{stem}_pdf.figures.json` (라벨 인접 재무 figure sweep, precision-first) + `## Detected Figures`. **불변식 준수**: 숫자는 mechanical(openpyxl/pdfplumber) only ([D-index-15](/ops/decisions) vision-boundary), 추출 fact 마다 citation(sheet!cell / page) 자동. **⭐ before/after (Apposter 20-sheet return model)**: BEFORE = Exit IRR `=XIRR(L85:Q85,L79:Q79)` 가 `+182 more` truncated 주석에 매몰, 기계추출 불가. AFTER = **31 key outputs 자동 surfacing** — Exit IRR **57.2%/50.4%**(Case B 49.6%/43.3%) · Exit MoM **8.60x/7.12x** · Entry EV **270억** · Exit **9x** EV/EBITDA, 전부 값+수식+라벨로 파일 상단. (직전 23-deal 의 Apposter agent 가 수작업 발굴한 수치가 이제 mechanical 추출.) test 40/40 PASS · 기존 포맷(docx/pptx/csv/image/zip) 무변경 · 신규 의존성 0 · datetime→ISO 직렬화 버그 fix 포함. orchestrator QC: agent draft 의 broad-recall false-positive(date cell·mislabel CAGR 25M) 를 numeric+rate 게이트로 34→31 정정(true output 전량 보존). **잔여 (L2-L4, 후속)**: L2 cells.json→index typed intake/financial_model **seed-draft 자동생성**, L3 cross-source reconciliation(deck vs xlsx vs cap table 불일치 flag), L4 versioned re-ingest diff. | 2026-05-30 |
| D-index-40 | **Render/리얼초이스(23rd) 적재 + par-entry cheap-option = E[CF] 극단 사례 ([D-index-25](/ops/decisions) poster child) — 데이터룸 소진** — 2026-05-30 사용자 "더 추가할 프로젝트?". 전체 데이터룸 스캔 → 적재가능 신규 = Render 1건. **Render = 리얼초이스/트루비아**(데일리퓨어크레아틴, 크레아틴 건기식 D2C). dataroom 有·IC 미완 → best-effort SCREEN, status='passed'(미투자 — ⚠ 리얼초이스/트루비아는 AXE **플랫폼 고객**(realchoice vault·Truvia frame 회계)이나 **고객 ≠ 투자집행**; 주금납입·조합결성 증거 없음). ⭐ **핵심 — par-entry cheap-option 이 E[CF] 를 오해소지로 끌어올림**: AXE 진입 = 5,000만 @ 100원 = **액면=순자산(93원) = par/book value**(트루비아 **벤처기업 확인 enabler** 목적 개인투자조합 신주, 정상 priced VC 라운드 아님). par 진입이라 **어떤 성공도 거대 multiple** → upside 28.0x(P 0.12). 결과 **E[CF] 35.13%**(랭킹 4위 수준)이나 **E[MoM] 4.53x·P(loss) 68%·median=손실(0.40x)**. ⭐ **fantasy([D-index-26](/ops/decisions))와 구분**: exit multiple 은 0.8-1.4x commodity 정합 scrub 완료(Oura/EGA식 인플레 아님) — 높은 E[CF] 는 **par 진입의 구조적 산물**이지 multiple 환상이 아님. 고로 Render 는 [D-index-25](/ops/decisions)("E[CF]=fat-tail winner 가 끄는 mean → E[MoM]+P(loss) 동반 필수")의 **poster child**: **E[CF] 단독 랭킹은 par-entry 딜에서 오해 유발** — Render 35% 는 top deal 이 아니라 **68% 손실확률 par 진입 cheap call option**. SCREEN=PASS(commodity supplement·moat 약함·median 손실·보통주 무보호). 단 5,000만 = 트루비아 벤처확인 enabler + 고객관계 cheap option 으로 재무 IC 와 **분리 의사결정** 가능. EGA(RCPS·매출16억·올리브영)·Nanora($9M cap)보다 early·고위험. deal_id `eb83b883`. **데이터룸 소진** (DB 23): 잔여 = Whale=위시켓(executed 포폴이나 원 IC/모델 부재, 자료 확보 시) · Purple AI/TR Corp/엘리시움(DHP 빈 폴더) · Prj_Artemis(AXE 자체 제품) · 델리후레쉬(RFP) · 이노씨앤에스/Curi AI/가람봇/딥트리/블루밍(thin lead) = 전부 비-딜/자료대기. **23-deal canonical E[CF] 랭킹** (⚠ par-entry 주의): Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > **Render 35.13(⚠par cheap-option, P loss 68%·median 손실)** > Canopy 31.8 > Interstellar 30.5 > Hancom 23.9 > EGA 23.3 > (Superman 20.8 별도) > Catalyst 16.99 > 데이톤 16.57 > Infinity 16.5 > Eduon 15.0 > 디벨로퍼그룹 14.76 > 수성별 14.0 > Sendy 11.7 > Nanora 10.1 > Novachips 9.2 > Apposter 8.34 > 유비랩 4.06 > Medistaff −0.59 > JS E&L −1.97 > Open Research −25.85. index commit `de47a2c`. | 2026-05-30 |
| D-index-39 | **Archive batch 6-deal 적재 (17~22th) — 전부 correctly-avoided(PASSED) — DB 22** — 2026-05-30 사용자 "1_Project (Archive) 6건 모두 best effort 검토". 6 병행 agent + orchestrator 양방향 QC. **전부 archived=미투자** → [D-index-24](/ops/decisions) calibrated canonical(surface 헤더 audit-trail). **딜별**: **Medistaff**(의료진 플랫폼, screen) E[CF] **−0.59%** — founder 형사 jeopardy(블랙리스트 방조·증거인멸 검찰송치) + 두나무 lead 선점. **Open Research/oo.ai** E[CF] **−25.85%**(P loss 90%) — ⭐ **"deal structure ≠ company quality"**: 회사는 Series A 2,000억 라운드 중이었으나 본 거래는 창업자 김일두(전 카카오브레인) **개인 distressed 담보 secondary bridge**(도박·사채 변제용) → 직후 도박/무단 지분매각/서비스중단/팀 9-of-9 이탈 표면화. 계약서 당사자 공란=미체결. **JS E&L**(2차전지 부품, transcribe) E[CF] **−1.97%** — EV 캐즘, 자본잠식 임박·누적결손 56.85억, surface exit PER 16x(2차전지 고배수)→NPC 2x·AJ 8x scrub. **Catalyst/클래스카드**(교육 PE buyout, transcribe) E[CF] **16.99%** — ⭐ index **2nd buyout + 첫 PEF/GP-structured**(AXE=GP, 삼일 PwC 옥션 입찰, committed=consortium equity 170억 ≠ AXE direct; GP commit ~5억+carry). surface Gross 30.86% → 10x flat exit를 교육 comp 5-9x로 scrub + 60% LTV wipeout. **"높은 IRR≠Go"**: first-time GP 가 600억 consortium 0 commit + 학령인구 + leverage. 미체결(SPA 없음, Mgmt/Exit 폴더 공란, archived before SPA). **데이톤/DATON**(DC AI Ops, screen) E[CF] **16.57%** — 흑자전환 100억 매출·DC tailwind·레퍼런스(에코프로BM/현대제철) 진성이나 terms-void(valuation/instrument 전부 assumed) + AINATION 합병 미확인. **Apposter/b.ring**(스마트링, transcribe) E[CF] **8.34%** — ⭐ **"field DD kills thesis"**: 현장실사(Bic Camera 방문 + McKinsey JP)가 moat 사망 확인(commodity 광동 OEM, sleep accuracy 열위, disposable test-buy) → **Case Drop**. surface Oura 10x PSR(2029 4,000억 exit) fantasy → Movano/Fitbit 2-3x scrub + 15yr 累損 wipeout. Nanora(10%) 대비 강한 traction·강한 kill. ⭐ **batch 교훈**: (1) **negative-E[CF] 기록 = PASS 결정의 정량 검증** — index 가 correctly-avoided 딜을 음수 what-if 로 보존(Open Research −25.85% / JS E&L −1.97% / Medistaff −0.59%), 회피 판단 근거화. (2) **양방향 QC 가 agent 에 전파** — 6 agent 가 instrument floor·fat-tail scrub·genuine wipeout 를 briefing 만으로 자체 적용([D-index-37](/ops/decisions)/[38](/ops/decisions)). (3) deal-class 다양화: buyout/PEF·founder-distressed-secondary·field-DD. **22-deal canonical E[CF] 랭킹**: Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > Interstellar 30.5 > Hancom 23.9 > EGA 23.3 > (Superman 20.8 별도) > Catalyst 16.99 > 데이톤 16.57 > Infinity 16.5 > Eduon 15.0(s) > 디벨로퍼그룹 14.76(buyout) > 수성별 14.0 > Sendy 11.7 > Nanora 10.1 > Novachips 9.2 > Apposter 8.34 > 유비랩 4.06(s) > Medistaff −0.59 > JS E&L −1.97 > Open Research −25.85. index commit `7d6ccca`. | 2026-05-30 |
| D-index-38 | **디벨로퍼그룹(15th, index 최초 buyout-class)·유비랩(16th, deep-tech screen) 2-deal batch — buyout FCF-floor + 양방향 QC** — 2026-05-30 사용자 "디벨로퍼그룹·유비랩 검토". 입력 richness 양극: 디벨로퍼그룹 = 완성 full IC(v5, 5개정) / 유비랩 = IR+Q&A only(screen). **(1) 디벨로퍼그룹 스터디카페 사업부 100% 영업양도** = index **최초 buyout-class**(PE형 캐시카우 M&A, AXE 12억/EBITDA 3.87x, 무차입, 100% 보유, 보통주/secondary). full IC surface 공식 확률가중 E[CF] **34.09%** 이나 **메모 스스로 Devils-reweighted 16.5% 명시**(17.6pp 갭) — Exit 5.0x(근거 'L', 비교사례 없음) + EBITDA 310M(이승호 대표 보수 미계상→실 230-260M + 수도권유지보수 83→23M -61.7% 급감 미규명) + Upside 10.53x "카카오/네이버 9,251M" fat-tail 의존. [D-index-24](/ops/decisions) 미집행→calibrated canonical + [D-index-26](/ops/decisions) fat-tail scrub(Exit 4.0x·조정EBITDA·kakao 폐기) + [D-index-21](/ops/decisions) genuine wipeout → **E[CF] 14.76% / E[MoM] 2.02x / P(loss) 15%** (메모 Devils 근방). ⭐ **buyout 신 insight**: cashflow buyout 의 FCF 배당이 실질 하방 floor(downside 도 ~1.1x, 원금손실 거의 없음) → **P(loss)가 VC 딜보다 구조적으로 낮음** (손실은 운영붕괴 wipeout 경로뿐). VC equity 와 다른 첫 자산군 — 단 SME 성숙기라 upside cap(IPO 5년내 불가). deal_id `b88e2dcb`. **(2) 유비랩**(그래핀/그라파이트 4단 적층 방열 시트, OLED foldable + AI 반도체 TIM) SCREEN: 병행 agent + orchestrator QC. **elite founder**(유봉현 — 삼성D R&D 33년·세계최초 82인치 UHD·美특허 120+·NeoGraf 美 VP) + **진성 traction**(삼성D 업체코드+1/2/3차 샘플매출+애플 샘플전달)이나 **component multiple cap**(회사가 Q&A 에서 지목한 직접경쟁사 신화인터텍 EV/Rev **0.2x**·PBR 0.51x) + pre-rev hockey-stick(92억@3y=2,500x, NeoGraf 글로벌1위도 $16M) + Samsung 단일고객 concentration → **"좋은 회사 ≠ 좋은 딜"** (Interstellar "높은 IRR≠Go"·Novachips "polish≠quality" 가족). ⭐ **양방향 QC ([D-index-37](/ops/decisions) 보완)**: D-index-37 교훈은 "낙관 메모 blind transcribe 금지"(아래로 scrub). 유비랩은 **반대 방향** — agent 가 base 를 as-converted(0.76x)로 계산해 E[CF] 2.9%/P(loss) **80%** 산출했으나, 본인이 가정한 RCPS 1x 우선권을 base(EV 143억 ≫ 20억 우선권)엔 미적용한 **내부 불일치 artifact**(downside 엔 적용). 우선권 floor 일관 적용(base 1.0x) → **E[CF] 4.06% / P(loss) 43%** (peer 정합: EGA 40%·에듀온 45%·Nanora 50%). **QC = 올바른 수치에 착지(단지 낮은 수치가 아님)** — fat-tail 도 instrument-floor artifact 도 모두 교정. deal_id `c98ba89e`, stage='Screening'. **16-deal canonical E[CF] 랭킹**: Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > Interstellar 30.5 > Hancom 23.9 > EGA 23.3 > Infinity 16.5 > Eduon 15.0(screen) > **디벨로퍼그룹 14.76(buyout)** > 수성별 14.0 > Sendy 11.7 > Nanora 10.1 > Novachips 9.2 > **유비랩 4.06(screen)** (+ Superman 20.8 별도). index commit `c0bf6d5`. | 2026-05-30 |
| D-index-37 | **EGA·수성별·Nanora 3-deal batch 적재 (12~14th) — transcribed 메모 rigor 가변 → QC+calibration 필수** — 2026-05-30 사용자 "EGA·Nanora·수성별 모두 진행" + "Nanora full ic". 3 병행 background agent(deal당 1) + orchestrator QC. **핵심 교훈 — full-IC 메모를 blind transcribe 하면 안 됨**: 메모 rigor 가 deal 마다 극과 극. **EGA**(디파이넘버, NMN 뉴트리코스메틱) 메모는 surface-optimistic → agent 가 그대로 옮긴 E[CF] **127%**(매출 16억→550억 3년 34x + EV/Rev 7× + 18x MoM@2y)는 [D-index-26](/ops/decisions) fat-tail 환상(Iippo 74% 재현). EGA 는 status='passed'(미투자·조건부)라 [D-index-24](/ops/decisions)상 surface 아닌 calibrated 가 canonical → **scrub**(revenue ramp 보수화 16→300/120억 + EV/Rev 5×/3.5×/2.5× K-beauty M&A anchor + **genuine wipeout 추가**: IR에 창업자 이름·이력 부재 + 매출 16억 미검증 red flag) → **calibrated E[CF] 23.3%** (E[MoM] 2.33x, P(loss) 40%). surface 는 헤더 audit trail 보존. 반면 **수성별**(국군복지단 DOOH 광고, 우선협상대상자) 메모는 이미 보수적 — Σp −23.8% → IRR(E[CF]) **+14.0%**(sign-flip, Interstellar 패턴). 단 **deal 조건 전부 dataroom 부재**(10억/30억/25% 가정) + 팀공백 + CAPEX gap 5:1 = info-void Conditional Pass (instrument TBD, series 기타). **Nanora**(US precision-wellness, TCM→구독 supplement+smart ring+AI, SAFE @ $9M post cap, pre-launch) 는 사용자 지시로 **SCREEN→FULL IC 승격**(rich dataroom): 18-section + multi-scenario + adversarial(proponent/premortem-critic 분리). research-calibrated E[CF] **10.05%**(E[MoM] 1.76x, P(loss) 50%) — 회사 plan을 **Care/of($225M→Bayer→2024 폐업) category-graveyard** + Oura 11x→supplement 1.5-2.5x rev + Pre-Seed→A 85% fail base-rate 로 하향. 판정 **Conditional**(미투자, 결정변수 = post-launch 90-day cohort retention, T+9M revisit). **QC 결론**: agent batch 는 효율적이나 transcribe 산출은 반드시 orchestrator 가 fat-tail/wipeout/base-rate QC (EGA 127%를 적재 전 차단). **14-deal canonical E[CF] 랭킹**: Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > Interstellar 30.5 > Hancom 23.9 > **EGA 23.3** > Infinity 16.5 > Eduon 15.0(screen) > **수성별 14.0** > Sendy 11.7 > **Nanora 10.1** > Novachips 9.2 (+ Superman 3-step 별도). index commit `fc56004`+`7b4a50d`. | 2026-05-30 |
| D-index-36 | **SCREENING 단계 정규화 — best-effort pre-DD 게이트 (research+모델 필수, full /ic 자원 gate)** — 2026-05-30 사용자 "screen 단계 추가 + 적합 IC memo + 최소 리서치 전부 + 재무모델 + best effort + 정규 프로세스화 + 적재". 1-pager(seed teaser)만 있는 inbound 에 19-agent `/ic`(수 시간·dataroom 전제)를 바로 돌리는 건 GIGO·낭비 → **Screening 게이트** 신설. **deal lifecycle**: sourcing → ⟨**Screening**⟩ → DD(full `/ic`) → IC 결의(Go/Conditional/Pass) → Portfolio → Exit. (deal.stage enum 에 'Screening' 이미 존재 — migration 불요.) **Screening 이 답하는 질문** = "이 건에 **DD 자원 투입 가치**가 있나?" (Go/No-Go 투자결정 아님). **프로토콜 (타협 금지)**: (a) **최소 외부 리서치 필수** — 시장·수요 / sector franchise-or-stage base-rate / exit comp(공개 multiple) / 경쟁·moat / founder 검증, (b) **재무모델은 회사 plan 이 아닌 base-rate calibrated**, (c) IRR 은 index engine IRR(E[CF]) (D-index-25/32) 로 검증, (d) premortem + 가정 라벨 A1..An (H/M/L). **산출물**: 딜폴더 `ic/{research,finance,memo}/` + index 적재 (stage='Screening', screen-pass 면 status='passed', what-if IRR 보존 [D-index-30](/ops/decisions)). **첫 사례 = 에듀온(EDUON)** — 초등 영어 Writing 프랜차이즈 Seed 보통주 5억@Pre 20억. ⭐ **교훈 — best-effort screen 이 ad-hoc 낙관을 base-rate 로 보정**: 회사 IRR 58% / Claude ad-hoc E[CF] 26.5%(upside 10x) → **research-calibrated E[CF] 15.0%** (E[MoM] 2.0x, P(loss) 45%). 두 회사 낙관(가맹 "3년 1,000" vs 3030영어 21년→1,400 / exit "매출100억→300억=3x rev" vs 크레버스 0.59x·웅진 0.20x rev)이 base-rate·comp 에 동시에 깨지며 upside 10x→6x cap. **Screen 판정 = PASS(DD 미진행)** — E[CF] 15%≪30% hurdle + SME 프랜차이즈(upside cap) + 보통주 무보호 + AI(Wizon) commodity. founder·product 우량하나 현 조건 DD 정당화 불가 → 재screen 조건(RCPS+Pre 10-12억+가맹 traction+vesting) + T+6M(론칭 후) 재screen. **정직 원칙**: 입력이 1-pager 면 "full cycle 의 정직한 형태 = Screen 에서 멈춤". 향후 `/ic` 에 lightweight screen-mode 통합 = [B-index-ic-screen-mode](/ops/backlog). index 11th deal (첫 Screening), deal_id `2bb5d585`. | 2026-05-30 |
| D-index-35 | **Interstellar (인티그레이션) 적재 — 10th deal, PASS (governance-driven, not IRR)** — 2026-05-30 사용자 "추가 적재 프로젝트?" → 데이터룸(OneDrive `1_Project/`) 스캔 결과 IC 분석 완료·미적재 = **Prj_Interstellar 1건** (Render=IC 미완 / Whale[위시켓]=보유 admin docs / 델리후레쉬=RFP 제휴건 / Pipeline=스크리닝 단계 — 모두 미대상). **인티그레이션**: 한의·치과 5-카테고리 플랫폼 (한의사 가입률 83% 사실상 단독 1위, FY24A 매출 454억 +74%). **Series D RCPS**, AXE follow 검토 20억 (신주 16.4+구주 3.6, pre 1,085억, AXE 1.6% O/S) — **PASS** (알토스+네이버 lead 160억 종결). [D-index-24](/ops/decisions): PASS→calibrated canonical (Sendy 동급, `status='passed'`, what-if IRR 보존). **canonical E[CF] IRR 30.5% / E[MoM] 3.23x / P(loss) 36.0%** (IC v1 surface Σp 계열 25.3%/2.71x). ⭐ **핵심 — PASS 사유가 IRR 아님**: E[CF] 30.5% 는 매력적(아래 9-deal 중 5위)이나 PASS 는 거버넌스(R6 자기거래 — 정희범 대표 명의 메디스트림 한의원+원외탕전 매출 인식 → IPO 심사 reject ≥30%) + AXE 1.6% minority(이사회 미진입) + 알토스/네이버 lead 후 marginal value 부재 = 순수 비-IRR. **"높은 IRR ≠ Go"** 명시 사례 (Novachips "polish ≠ quality" 와 짝). **현 canonical 9-deal 랭킹** ([D-index-34](/ops/decisions) 8-deal supersede): Sentry 43.5 > Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > **Interstellar 30.5** > Hancom 23.9 > Infinity 16.5 > Sendy 11.7 > Novachips 9.2. 부수: `seed.rs` SERIES_OK 에 Series D/E 추가 (누락 — 향후 Series D+ intake hard-reject 방지). validate-seeds 12 Pass / 23 test / DB 10 deal. index commit `7269286`. | 2026-05-30 |
| D-index-34 | **canonical 8-deal E[CF] 랭킹 확정 — Σp/surface 시절 랭킹 supersede** — 2026-05-29 [D-index-32](/ops/decisions)/[33](/ops/decisions) 후속, [B-index-docs-irr-ecf-restate](/ops/backlog) 수행. D-index-19/21/23/24 의 인라인 랭킹은 Σp·IRR(폐기) 또는 surface 기준 → engine `validate-seeds` 로 9 deal IRR(E[CF]) 재산출하여 canonical 확정. **canonical E[CF] 랭킹**: Sentry 43.5 ≈ Iippo 43.3 > Starnex 38.1 > Canopy 31.8 > Hancom 23.9 > Infinity 16.5 > **Sendy 11.7 > Novachips 9.2**. **구 Σp 랭킹 대비 변화**: (a) **tail flip** — Sendy(구 6.5 최하위) 가 Novachips(구 7.8) 위로 (E[CF] 11.7 vs 9.2); (b) Sentry–Iippo 격차 4pp→0.2pp (거의 동률); (c) 전반적으로 E[CF] 가 Σp 보다 높음 (fat-tail upside 가 mean 견인). **per-deal 구(Σp/surface)→신(E[CF])**: Sentry 40.3→43.5 / Iippo 36.5→43.3 / Starnex 36.4→38.1 / Canopy 26.4→31.8 / Hancom 22.4→23.9 / Infinity 13.8→16.5 / Novachips 7.8→9.2 / Sendy 6.5→11.7. overlay: Infinity recalibrated -8.3→-4.3 / Superman fresh 10.5→20.8 ([D-index-33](/ops/decisions) 에서 seed 전환 완료). **Infinity 특례 (executed deal — [D-index-24](/ops/decisions)↔[32](/ops/decisions) 조정)**: v8 **모델(leaves) 불변** 유지(집행건 decision-time 보존) + 집계만 E[CF] 통일 → canonical metric = **E[CF] 16.5%** (baseline 0.1646), IC headline **13.8%** 는 provenance (집계법 차이, 모델 자체 불변). 즉 D-index-24 의 "사후 재작성 금지" = 모델/가정 보존이지 집계법 고정이 아님. **seed 주석도 정합** (index commit `537c04e`: starnex/hancom/sendy/infinity 주석 E[CF] 재기재, value 불변). **역사적 결정문(D-index-19/21/23/24)**은 revision 최소화 — 인라인 숫자는 당시 기록 보존, 본 결정이 현 canonical 랭킹 SoT. | 2026-05-29 |
| D-index-33 | **irr_instrument_adjusted 폐기 — instrument 효과는 leaf waterfall 로만 표현 (flat discount 이중계상)** — 2026-05-29 사용자 "수익성 정확도 심화" 방향 선택 후 조사 결과. **발견**: exit_matrix leaf recovery 가 instrument 구조를 포트폴리오 전역에서 이미 반영 — RCPS deal 은 preference floor (Sentry 0.05~0.14 / Starnex 회생 1.10x "RCPS floor"), 보통주 deal 은 후순위 wipeout (Hancom 0.30 / Sendy 0.15 / Novachips 0.20 / Infinity 0.12, leaf 설명에 "보통주 후순위" 명시). 따라서 **IRR(E[CF]) 가 이미 instrument-aware**. Novachips 1건에만 있던 `irr_instrument_adjusted` (E[CF] +9.2% 에서 -12pp 추가 → -4.55%) 는 leaf 의 보통주 wipeout(0.2x)을 **이중계상** (검산: E[MoM] 1.34x → E[CF] +9.2%, wipeout 6M@5y 이미 포함). [D-index-19](/ops/decisions) 의 instrument-adjusted-IRR 제안([B-index-instrument-adjusted-irr](/ops/backlog)) supersede — discount table 미구현. **변경**: (a) novachips `irr_instrument_adjusted` baseline 제거 + 헤더 재프레이밍; (b) `seed.rs::validate` DEPRECATED_OUTPUT_CODES load-time hard reject — `irr_instrument_adjusted` (D-index-33) + `irr_loss_included` / `irr_success_only` (Σp, [D-index-32](/ops/decisions) 완결, 음성 테스트 OK); (c) overlay seed 잔존 Σp output_code 정합 — infinity_recalibrated / superman_fresh 의 `irr_loss_included` → `irr_expected_cashflow` (engine E[CF]: **Infinity -4.3%** [구 Σp -8.3%] / **Superman fresh 20.8%** [구 Σp 10.5%] — fat-tail 일수록 Σp↔E[CF] 격차 큼). **잔여 (정직 기록)**: docs 8-deal 랭킹 등 IRR 다수가 pre-E[CF] (Σp/surface) → [B-index-docs-irr-ecf-restate](/ops/backlog). Σp·IRR 과 동형 (잘못된 별도 집계 폐기). 11 seed validate + 23 test + 음성 테스트 Pass. index commit `74e1eb4`. | 2026-05-29 |
| D-index-32 | **Σp·IRR 폐기 — IRR(E[CF]) 단일 canonical 집계** — 2026-05-29 사용자 승인 "p IRR 폐기하고 남은 것 모두 진행" ([D-index-25](/ops/decisions) 의 "폐기 보류" 해제). **배경**: Σp·IRR (per-scenario IRR 의 확률가중 평균) 은 "수익률을 평균" 하는 잘못된 집계 — fat-tail deal 에서 rate-averaging 이 mean 을 왜곡. IRR(E[CF]) (확률을 cash flow 에 먼저 적용 후 IRR) 만 canonical 로 유지. **변경**: (a) `query_irr` summary 에서 `weighted_irr_loss_included` / `weighted_irr_success_only` 제거 → `irr_expected_cashflow` + `expected_moic` + `prob_loss` + `success_probability` + `methodology` 만 노출; (b) `src/irr.rs::weighted_irr` / `weighted_irr_success_only` 에 `#[deprecated]` + `#[allow(dead_code)]` (호출부 0, 산식은 audit 위해 보존); (c) **E[MoM] (weighted_moic) 는 폐기 아님** — MoIC 는 linear 집계라 확률가중 평균이 정합 (rate 가 아닌 multiple); (d) `validate.rs` ValidateReport + 9 seed `financial_outputs_baseline` 의 output_code 를 `irr_expected_cashflow` 로 전환 (E[CF] 값); (e) html/xlsx cover 의 IrrSummary 표시 = E[CF] / E[MoM] / P(loss). 23 unit test + validate 3 PASS. index commit `3b2f543`. | 2026-05-29 |
| D-index-31b | **share_type 신주(primary)/구주(secondary) 구분 + post-money invariant** — 2026-05-29 사용자 "round size 와 entry 가 신주 구주 나눠진 경우도 수용 가능한가 → 신·구주 혼합 등 다양한 상황 많다". **모델**: `fund_investment.share_type ∈ {primary\|secondary}` — **primary**(신주) 는 회사로 자금 유입 → post-money 증가에 반영, **secondary**(구주) 는 기존 주주 지분 매입 → post 불변 (회사 미유입). 혼합 라운드는 `round_primary_krw` (라운드 중 신주분) 별도 기재. **invariant** (`SeedFile::validate` Check 1b + `validate.rs`): `post_money ≈ pre_money + 신주(round_primary)` ±2% — 위반 시 hard reject. **검증 효과**: Infinity pre 499.5억 / post 500억 이 DSC 라운드 100억 신주와 불일치 (500 ≠ 499.5 + 100) → invariant 가 포착 → pre 400억 으로 정정 (post 500 = pre 400 + 신주 100). Hancom / Novachips 는 구주 매입분 포함 → `share_type=secondary`. migration 0005 = share_type + round_primary_krw. index commit `98b88c2`. | 2026-05-29 |
| D-index-31 | **투자성 분석 최소 intake 게이트 (5 필수 필드) + EV/EqV 혼동 차단 + _TEMPLATE** — 2026-05-29 사용자 "투자성 분석 시 최소 필요한 정보는 받아서 시작 — entry date / valuation (EV 인지 EqV 인지 혼동 없게) / 전체 라운드 / 투자 규모 / Series". **5 필수 필드** (`SeedFile::validate` Check 1b): `series` + `round_size_krw` (라운드 전체) + `committed_krw` (우리 투자) + `entry_date` + `pre_money_krw`. **enforcement 2-tier** (사용자 "Warn 기존 호환 + 신규만 hard"): `intake_enforced=true` (신규/template) → 누락 시 hard reject; `=false` (기존 9건) → warn 만. **EV/EqV 규칙** (혼동 영구 차단): entry valuation (pre/post) 은 **항상 EqV** (지분율 분모) — EV 입력 금지; exit 만 `proceeds_basis` 로 EV→EqV bridge ([D-index-22](/ops/decisions)). **신주/구주**: round 을 primary/secondary 로 분해 ([D-index-31b](/ops/decisions)). 산출물 = `seeds/_TEMPLATE.yaml` (intake form + EV/EqV 가이드 주석 + 신주/구주 혼합 예시). migration 0004 = series + round_size_krw. 기존 9건 backfill (series + round_size_krw + share_type). index commit `c4c12f4` + `9e86a07`. | 2026-05-29 |
| D-index-30 | **status='passed' (검토 미투자) — what-if IRR 보존 + fund/project baseline 제외 + 5/29 배치 정정** — 2026-05-29 사용자 "pass 도 당연히 IRR 로 데이터 관리. fund·project 수익성에서는 제외". **status='passed'** = 검토했으나 미집행 — per-position what-if IRR 은 **계산·보존** (의사결정 학습용) 하되 `committed_total` / fund pooling / project baseline 에서 **제외**. **all-passed fallback**: 한 deal 의 position 이 전부 passed 면 (실투자 0) baseline 계산 시 passed 를 포함 (빈 집계 방지). 구현 = `incl()` closure (`status != "option" && (!has_real \|\| status != "passed")`) 를 mcp.rs query_irr + validate.rs 에 공통 적용. **5/29 사용자 배치 정정 동반**: (a) Sentry Pre-A = 투자조합 1호; (b) Iippo 1호=1호·2호=2호; (c) Starnex 검토 20억 / entry 8월말; (d) **Canopy BW 는 별도 deal 아닌 Canopy 안의 passed position** (BW entry 2026-02-28, 4.2% warrant — per-position 모델로 BW IRR 28.0% &lt; RCPS 31.8% 정합, 이전 별도 deal CanopyBW 삭제); (e) Hancom 검토 5억 / 납입 7월말 / passed; (f) Sendy 검토 10억 / Pre-C / passed; (g) Novachips entry 2026-06-10 / passed. **proceeds 비례 재척도** (사용자 "IRR 보존") — 규모 변경 시 leaf proceeds × (new/old) 로 IRR 불변 유지 (Starnex 38.1% / Hancom 23.9% / Sendy 11.7%). migration 0003 = passed status. index commit `f650bec`. | 2026-05-29 |
| D-index-29 | **status='option' (pro-rata 권리 미행사) — baseline·fund·per-position IRR 전면 제외** — 2026-05-29 사용자 "Series A Option 은 무슨 표현?" 후속 재분류. **status='option'** = 향후 행사 가능한 pro-rata 권리 (아직 집행도 검토결정도 아님) — `committed_total` / baseline / recovery / fund pooling 에서 **전면 제외**, per-position IRR = null (행사 전이라 cashflow 미존재). passed ([D-index-30](/ops/decisions)) 와의 차이: passed 는 검토 완료·what-if IRR 보존, option 은 권리만 보유·IRR 미산출. 대표 케이스 = **Sentry 후속 5억 pro-rata option** (1호 단독 5억 집행 + 후속은 권리만) → status='option' 으로 재분류 (이전 committed 로 잘못 집계되던 것 제거). fund_investment.status enum 최종 = option / passed / committed / paid / locked_in / written_off / partial_exited / exited. migration 0002 = option status. index commit `9523638`. | 2026-05-29 |
| D-index-28 | **미집행 position entry timing — planned date + never-silent flag + fund pooling 클램프 버그 수정** — 2026-05-29 사용자 지적 "2호 납입 시점이 한 달 다른데 IRR 이 완전 동일한 게 말이 되나". **진단**: 계산식은 정확 (격리 테스트 IippoTT — 2호 +1개월 44.24% / +4개월 47.40%, MoIC 5.685x 불변; exit=회사 단일 청산 calendar 고정이라 늦은 entry=짧은 hold=높은 IRR). 동일값 원인 = **2호 paid_date=null → 코드가 offset 0 으로 silent fallback → 1호와 hold 동일**. **수정 3종**: (a) `seeds/iippo.yaml` 2호 `entry_date=2026-06-15`(가안/planned, 사용자 결정), `paid_date=null` 유지(미집행) → 코드 `paid_date.or(entry_date)` fallback → 2호 IRR **44.24%** (1호 43.27%, +0.97pp). (b) **never-silent flag** — `entry_basis ∈ {paid\|planned\|assumed}` + note (paid_date 없으면 "⚠ 미집행 … 보수적 하한" 명시; query_irr per_fund + fund_performance per_position). (c) **fund_ecf_irr 풀링 버그** — `deal_ref_offset` 의 `.max(0.0)` 제거: fund position 이 deal lead 보다 늦게 진입(deal_ref &lt; fund_epoch)하면 음수 offset 이 정상인데 클램프가 hold 항등식(exit_t − paid_offset = leaf.timing − deal_paid_offset)을 깨 단일 position 펀드 IRR(43.27%)이 standalone(44.24%)과 불일치 → 수정 후 일치. **부수**: flag 가 Hancom/Novachips/Sendy/Starnex/Superman entry date 누락(`assumed`) 노출 (이전 silent) → [B-index-entry-date-backfill](/ops/backlog). index commit `85c8481`. | 2026-05-29 |
| D-index-27 | **per-position / per-fund IRR (투자조합 1·2·3호 수익성) + 추가투자 = 별도 position + Iippo 2호 = AXE Private Fund II** — 2026-05-29 사용자 4 지시 "추가 투자가 별도 건으로 관리 / 1·2·3호 펀드 수익성 / Iippo 2호 별도 적재·분석" 의 답. **per-position IRR**: `fund_investment` row 별 독립 IRR — proceeds split = non-writedown `leaf.proceeds × (own_fd/ref_own)`, writedown `recovery × own_committed`; timing = `leaf.timing − deal_paid_offset` (position 납입이 deal lead 보다 늦으면 hold 단축). `src/irr.rs::position_ecf_irr / position_moic`. **per-fund IRR**: `fund_performance(fund_entity_id)` MCP tool — 펀드의 모든 deal 횡단 position pool → 단일 calendar E[CF] IRR (`fund_ecf_irr`). fund_entity_id = **axe_ia_001**(1호 블라인드) / **axe_ia_002**(2호) / **axe_ia_003**(3호). **Iippo 2호 정정**: 가공했던 "Series A follow-on" 제거 → 실제 = **AXE Private Fund II (개인투자조합)** 통한 Pre-A 동일 라운드 1억 RCPS 공동투자 (GP 액스벤처스/강태훈, LP=Iippo측 유치 개인 + AXE 100M, MOSS 미등록·6 PCC·Conditional GO 미집행). 별도 IC Memo = OneDrive `Prj_Iippo/ic/memo/Iippo_2호_FundII_IC_memo_v1.md` (vehicle 심의, deal thesis = IC v8). index commit `7e313b5`. | 2026-05-29 |
| D-index-26 | **fat-tail optimistic leg comp-scrub → canonical (overlay 아님) — Iippo/Sentry/Canopy 74% 환상 제거** — 2026-05-29 사용자 "투자성 재검토 필요한 건 없나 → 2번 수행 (upside-leg 재검토)" + "기대현금흐름 IRR 아까 수정 안 됐나" 의 답. [D-index-25](/ops/decisions) IRR(E[CF]) 도입 후 fat-tail multi-leg deal 의 mean 이 mgmt/upside fantasy leg 에 과민 → 외부 comp 로 cap. **scrub (comp anchor)**: (a) **Iippo** mgmt EV 1~2.5조 → 1,000~2,500억 (강남언니 752억→6,000억 ≈ 8x ceiling); IRR(E[CF]) **74% → 43.3%**, E[MoM] 14.9x → 5.68x. (b) **Sentry** upside axe 236억(=47x) → 73억 (마스턴 27조 AUM ≈ 3,000~5,000억 equity), mgmt 84.5억 → 58.65억; **43.6%**, success-only 59.9%/16.8x → 45.5%/8.43x. (c) **Canopy** fleet EV/EBITDA 12x → 6x (Bird $2.5bn→$145M de-rating), non-writedown proceeds ×0.5; **31.8%**. **핵심**: scrub 을 **canonical seed 에 직접 적용** (이전엔 overlay-only → query 가 un-scrub 74% 노출 → 사용자 지적). 각 seed `financial_outputs_baseline` 주석에 **IC 원본(pre-scrub) 보존** (Iippo IC v8 37.0%/14.9x, Sentry IC pass 41%/13.2x, Canopy 27%) — audit trail. [D-index-24](/ops/decisions)(executed=IC canonical) 와 양립: leaf scrub 은 live E[CF] 계산 입력, 원본은 추적 가능. index commit `8c5e60e`. | 2026-05-29 |
| D-index-25 | **기대현금흐름 IRR (IRR of E[CF]) = canonical 수익성 지표; Σp·IRR 은 병기 (폐기 보류)** — 2026-05-29 사용자 질의 "확률 고려한 cashflow IRR 의 정확한 표현은?" + "cashflow IRR 이 확률 내포면 그게 더 정합" 의 답. **두 집계 구분**: (a) **Σp·IRR** (per-scenario IRR 의 확률가중 평균) = "수익률의 평균" — 잘못된 집계 (rate 를 평균), legacy; (b) **기대현금흐름 IRR = IRR(E[CF])** = 확률을 cash flow 에 먼저 적용 → 그 기대 CF stream 의 IRR — **canonical**. toy: 50% 10x + 50% 0.1x → Σp·IRR 10.8% vs IRR(E[CF]) 38.5% vs E[MoM] 5.05x. IRR(E[CF]) 는 fat tail 이 carry 하는 **평균값** → 반드시 **E[MoM] + P(loss MoM&lt;1) / median 과 묶어** 제시 (단독이면 우상향 왜곡). 구현 `src/irr.rs::expected_cashflow_irr(committed, leaves)` = leaf timing 별 group → E[CF] → compute_irr; `prob_loss`. query_irr summary = `irr_expected_cashflow`(canonical) + `expected_moic` + `prob_loss` + legacy `weighted_irr_*` 병기. **Σp·IRR 폐기는 사용자 승인 후** (현재 보류 — "우선 추후 폐기") → [B-index-deprecate-sump-irr](/ops/backlog). index commit `2d2066f`. | 2026-05-29 |
| D-index-24 | **executed deal = IC-approved 기준 canonical (decision-time 기록 보존); 현 practice 재calibration = retrospective overlay** — 2026-05-29 사용자 지시 "우선 13.8% 로 IC 승인 후 집행한 건이므로 13.8% 를 적재하라". [D-index-23](/ops/decisions) Infinity 3-step 비교(surface 13.8% → reviewed −8.3%) 후속 — **어느 값이 canonical 인가**의 결정. **원칙**: `deal.stage` 가 **Closed/집행됨**이면 canonical DB 기록 = **IC 가 실제 승인·의사결정한 기준** (Infinity 13.8%, v8 FINAL irr_locked) — 집행된 deal 의 IRR 은 **사후 재작성하지 않는다** (회수 시 actual 대조 baseline + 의사결정 history 무결성 보존). 현 practice 재calibration(−8.3%) 은 `seeds/infinity_recalibrated.yaml` + v9 memo 에 **retrospective overlay** 로 보존 (post-mortem·learning lens, DB 미적재 — 동일 deal_code 충돌). **executed-vs-passed 규칙**: **Sendy(Passed)** = 집행 결정 부재 → calibrated(6.5%)가 canonical (보존할 의사결정 없음, [D-index-21](/ops/decisions)); **Infinity(Closed)** = IC-approved(13.8%)가 canonical, recalibration 은 overlay. 즉 index canonical 선택 = **(passed → 현 practice 재작성 canonical) vs (executed → decision-time 기록 canonical + 별도 overlay)**. **8-deal canonical 랭킹** (⚠ pre-E[CF] Σp 기준 — 현 canonical 랭킹은 [D-index-34](/ops/decisions) E[CF]): Sentry 40.3 > Iippo 36.5 > Starnex 36.4 > Canopy 26.4 > Hancom 23.2 > **Infinity 13.8** > Novachips 7.8 > Sendy 6.5 (overlay 적용 시 Infinity −8.3 최하위). seed: `infinity.yaml`(canonical 13.8% v8) + `infinity_recalibrated.yaml`(overlay −8.3% v9). index commit (canonical swap) 후속. | 2026-05-29 |
| D-index-23 | **3-step 검증 프로토콜 (surface 적재 → 현 practice 재수행 → 비교) — Prj_Infinity(INEX) 13.8% → −8.3% sign-flip** — 2026-05-29 사용자 지시 "기존 데이터 적재 테스트 → 신규 IC 재수행 → 두개 비교로 전 건들과 같게". [D-index-19](/ops/decisions)(surface↔calibrated gap)·[D-index-21](/ops/decisions)(현 practice 재작성) 를 **명시적 3-step 으로 정식화** — surface 를 별도 seed 로 적재(audit snapshot)하여 gap 을 DB 에서 정량화. **대상 INEX** (가상자산 거래소/VASP, 보통주 bridge 0.4997억 / POST 500억 / Closed). **Step 1 (surface)**: v8 FINAL(irr_locked) 그대로 (= 현 canonical `infinity.yaml`) → index 가 **13.8% / MoM 1.70x / success 76.5%** 정확 재현. **Step 2 (현 practice 재수행 + 외부 research)**: 4 anchor — (a) **실명계좌 base-rate near-zero** (2021 특금법 후 원화마켓 신규 진입 0건, 5사 closed club, FIU 옥죔, P&lt;10%) → surface success 76.5% 는 mgmt optimism anchor, (b) 코빗 won-market ceiling **1,000-1,400억**(Mirae MOU 25.12) → INEX coin-only 는 그 below, (c) **Gopax/Binance license-shell M&A 선례** (실패 coin-only entity 도 SI 매수, 2.7yr 규제 slog + discount) → 모달 exit = won-market rerating 이 아닌 **SI shell M&A breakeven**, (d) **보통주 distress recovery 0~20%** (RCPS 우선권 후순위) → genuine wipeout leg. **Step 3 (비교)**: calibrated `infinity_recalibrated.yaml`(overlay) = **−8.3% / MoM 0.82x** (upside 0.10 / base 0.42 breakeven / downside 0.30 / **wipeout 0.18 @ 0.12x**). **~22pp gap + sign-flip.** **핵심 시사점**: **성숙·정직한 IC memo 도 핵심 base-rate 를 mgmt optimism 에 anchor 하면 결론 역전** — v8 은 Event-Driven all-or-nothing 명시 + **종합 IRR 13.8% < hurdle 20% 공개** + 3 reviewer + irr_locked 로 매우 성숙했으나, success 확률을 실명계좌 optimism(실제 base-rate &lt;10%)에 anchor + 보통주 wipeout leg 누락 → reviewed E[IRR] sign-flip. **"return 이 hurdle 미달임을 공개"하는 것과 "probability 가 base-rate anchored"인 것은 별개** — index value = base-rate-anchored 확률 + genuine wipeout 강제 (surface honesty 와 독립). **proceeds 메커니즘은 v8 이 이미 정확** (ev_bridge·net debt 0 보통주·exit-diluted 0.0724% — [D-index-22](/ops/decisions) 모범) → 재계산 0, gap 은 순수 analytical. **8-deal 랭킹** (⚠ pre-E[CF] Σp 기준 — 현 canonical [D-index-34](/ops/decisions)): **canonical = IC-approved 13.8%** (executed deal — [D-index-24](/ops/decisions)): Sentry 40.3 > Iippo 36.5 > Starnex 36.4 > Canopy 26.4 > Hancom 23.2 > **Infinity 13.8** > Novachips 7.8 > Sendy 6.5; recalibration overlay(−8.3%) 적용 시 Infinity 최하위. 단 INEX 는 조합 6.6%(0.5억) Event-Driven 소액 optionality bet 라 invested 자체는 방어 가능 (license scarcity + SI LOI floor). 산출물: OneDrive `Prj_Infinity/ic/memo/v9_axe_current_practice_260529.md` + seed `infinity.yaml`(canonical 13.8% v8) + `infinity_recalibrated.yaml`(overlay −8.3% v9). index commit `c0dcd9a` + canonical swap. | 2026-05-29 |
| D-index-22 | **proceeds 산정 강화 — EV→EqV(net debt) bridge + exit-dilution 명시·강제 (manual scalar 불가)** — 2026-05-29 사용자 audit "모두 EV = EqV + Net Debt 개념과 투자 후 dilution 을 적절히 고려해 계산됐는지 체크" 의 답. **발견 — 7-deal proceeds 도출이 비일관**: (a) net-debt bridge = **Canopy 만 명시** (EV 797억 − net debt 300억 = EqV 498억; KB캐피탈 fleet 차입이라 가장 필요한 deal 을 옳게 처리), Sendy(v2)·Iippo 는 `stake × EV` 직접 (EqV bridge 누락), pre-IPO 3 + Sentry 는 시총/PER equity multiple (bridge 구조적 N/A — 정상); (b) dilution = pre-IPO 3 + Sendy 는 exit-date 희석 stake 명시, **Iippo·Sentry 는 entry F/D flat** (중간 라운드 희석 미반영 → proceeds 수 pp 과대 낙관). **결정 ([D-index-20](/ops/decisions) 의 "IRR deterministic·override 불가" 를 proceeds 로 확장)**: proceeds 도 manual scalar 가 아닌 명시 도출. `financial_model.proceeds_basis` enum 4종 — **ev_bridge** = `exit_stake × (exit_ev − exit_net_debt)` / **equity_value** = 시총·PER (equity multiple, net-debt N/A) / **mom** = `committed × MoM` (pre-IPO check 기반) / **legacy_ev** = EV-based pre-hardening (per-leaf bridge 미itemize, retrofit 대기). **강제 2중**: ev_bridge non-writedown leaf 는 `stake×(EV−net debt)` 가 axe_proceeds 와 ±2% 일치해야 — (1) seed load-time **hard reject** (`SeedFile::validate`) + (2) `validate_financial_model` **Check 7 `exit_proceeds.bridge` Error**. negative test 로 거부 확인 (stake 10% × EV 2,225억 = 222.5억 ≠ 날조 300억 → "proceeds 불일치 >2%" reject). migration 0005 = proceeds_basis + exit_ev_krw/exit_net_debt_krw/exit_stake_pct 컬럼. **Sendy 정정 (v3)**: proceeds_basis=ev_bridge, **net debt = 0** (asset-light 플랫폼 트럭 미보유 + 자금니즈를 debt 아닌 equity[Series C]로 충족 + dataroom 차입 증거 0 → 보수적; **없는 부채 날조 안 함**), exit_stake 10% = entry 12.5% 의 20% 희석. → **proceeds·IRR 불변 (E[IRR] 6.47%)** 이나 EV≠EqV 누락 class 를 구조적으로 영구 차단. "정정 = 구조적 (bridge 명시 + 엔진 강제), 숫자 불변" 이 evidence 상 정직한 결과. **7-deal 결과**: Sendy ev_bridge PASS(3 leaf reconciled) / Sentry equity_value PASS / Starnex·Hancom·Novachips mom PASS / Iippo·Canopy legacy_ev WARN — **0 errors**. **잔여 gap (정직 기록 → [known-gaps](/ops/known-gaps))**: (1) Iippo·Canopy per-leaf EV→EqV bridge 미itemize → [B-index-proceeds-bridge-retrofit](/ops/backlog), (2) Iippo·Sentry exit-dilution = entry F/D flat (중간 라운드 희석 미반영; RCPS anti-dilution/pro-rata 부분 상쇄 — Closed deal 이라 사후 실익 낮으나 IRR-정직성 차원). index commit `ca714b7`. | 2026-05-29 |
| D-index-21 | **historical deal 축적 = 현 practice 재작성 (raw 축적 아님) — 3-tier IRR + v1 5-flaw 교정** — 2026-05-29 사용자 redirect "그냥 적재하지 말고 ic memo 를 거의 현재 우리 practice 수준으로 새로 써본다 생각하고 임하라 (당시 작성 방법론은 현재보다 열위일 가능성 높음, 추가 조사 가능)" 의 답. [D-index-19](/ops/decisions) 가 "xlsx surface cell → calibrated" 였다면 본 결정은 그 논리적 종착 — **전체 memo 방법론 자체를 현 5-tenet practice 로 재작성**. **3-tier IRR story** (index 의 reviewed-truth 가치 실증): (a) v1 md 기재 = Expected 31% / Devils 22.8% (per-scenario 62/35/-8, Excel 과 모순된 **fabricated override** — D-index-20 가 architectural 차단), (b) v1 Excel 실제 = Base 95.7% / weighted 92.4% (**math-correct 이나 낙관 input** — D-index-20 이 자동 산출), (c) **v2 현 practice 재calibration = E[IRR] +6.5% ⭐ canonical** (외부 comps anchor + genuine wipeout leg). **v1 의 5 결함 교정**: (1) IRR override 95.7→35 fabrication (D-index-20), (2) **wipeout leg 부재** — v1 의 Downside 가 +31.6% gain 이라 손실 시나리오 자체 없음 → genuine 18% wipeout leg 추가 (보통주 floor 0 / 부릉 1/6 precedent recovery 0.15x), (3) **sendyX 66x 미검증** (beta 25.12 고객 5곳 월 375만원 → 2026E 25억 = 1년 66배 bottom-up 무근거) → base-rate haircut (mgmt 919억 대비 -93% deep-discount, B2B SaaS PMF base rate 정합), (4) **Exit 5x flat → segment-weighted** (freight matching 1.0~1.5x + TMS SaaS 4~5x; WiseTech-E2open 3.5x M&A anchor), (5) instrument 분석 부재. **외부 anchor** (background research): freight matching EV/Rev 0.3~2x, TMS SaaS 3.5~5x, Convoy $3.8B→0 + Mesh Korea(부릉) ₩5,500억→₩800억(1/6 distress) → wipeout base rate 15~25%, 한국 Series C→IPO 예외적 (TMAP 미들마일 IPO covenant 미달 교훈 → Bull IPO P 0.12 로 down). **결과**: 4-scenario bull(0.12, 4.45x, IRR +64.5%) / base(0.40, 1.83x, +16.3%) / bear(0.30, 0.72x, -7.0%) / wipeout(0.18, 0.15x, -31.5%) → **E[IRR] 6.5% / E[MoM] 1.51x / σ 27.5% / Sharpe-like -0.06 / Semi-σ 17.7%**. PASS 재확인 — v1 사유(구조+타이밍)에 더해 fundamentals 도 30% hurdle -23.5pp 미달로 정합 (당시 quality 평가는 긍정이었으나 현 practice 로는 marginal). **portfolio 영향**: 7-deal 랭킹 Sentry 40.3 > Iippo 36.5 > Starnex 36.4 > Canopy 26.4 > Hancom 23.2 > Novachips 7.8 > **Sendy 6.5 (최하위)** — v1 의 92% 매력은 대부분 2 낙관 input + 누락 risk leg 의 산물이었음을 정량 노출. **일반 원칙 (index 축적 protocol 확장)**: (1) deal 의 IC 방법론이 현 practice 이전이면 conclusion 축적 아닌 memo 재작성, (2) 외부 comps/precedent 로 multiple·probability anchor, (3) genuine downside/wipeout leg 필수 (낙관 일변 금지), (4) unvalidated bottom-up 가정은 base-rate haircut, (5) v1 conclusion 은 audit citation (financial_outputs_baseline) 으로만 보존. 산출물: OneDrive `1_Project/Pipeline/Sendy/ic/memo/v2_axe_current_practice_260529.md` (5-tenet · 4-scenario · deterministic IRR · premortem · v1→v2 교정표) + seed v2 = `/Users/axe/index/seeds/sendy.yaml` (financial_model version:2). | 2026-05-29 |
| D-index-20 | **manual IRR override 불가 (deterministic compute SoT) + declined-deal (Passed stage) tracking** — 2026-05-29 센디(Sendy) 축적 시 발견된 flagship 사례, **index 존재 이유의 결정적 증거**. 센디 `ic/finance/exit_matrix/v1.yaml` line 76: `irr_approx_pct: 95 # (375/50)^(1/3)-1 ≈ corrected: 35%` — 계산값 95.7% 를 근거 없이 "35%" 로 손수 override. 이 오류가 md 전반 전파 (Base 35% / Downside -8% / Expected 31% / Devils 22.8%), 그 중 **Downside -8% 는 구조적 불가** (수취 114억 > 투자 50억 → IRR 반드시 양수). 문서 자체가 "수정 필요 상태 (현재 미수정)" 명시했으나 미교정 잔존. **index 정책**: IRR 은 `exit_matrix_leaf` 의 proceeds + timing 에서 `single_flow_irr` deterministic 계산 — financial_output 에 IRR 직접 쓰는 path 없음 (manual override 물리적 불가). 센디 seed (v1) 를 EV-derived proceeds (Exit EV × stake 10%) 로 작성 → index 가 Base 95.7% / Downside 31.6% / Upside 146.6% / weighted 92.43% 자동 산출 = Excel 실제값 정확히 일치 (md override 자동 교정). md 의 erroneous 값은 financial_outputs_baseline citation 으로만 보존 (audit). **단 이 92.43% 는 math-correct 이나 v1 의 낙관 input (sendyX 66x + Exit 5x flat + wipeout leg 부재) 산물 — 초기 축적값일 뿐 canonical 아님**; 사용자 redirect 로 현 practice 재작성 → seed v2 = E[IRR] 6.5% 로 supersede ([D-index-21](/ops/decisions)). 즉 본 결정은 "computed override 차단" (architecture), D-index-21 은 "낙관 input 자체를 현 practice 로 재calibration" (analysis) — 2-layer. **부수 결정 — `deal.stage` 에 'Passed' 추가** (migration 0004): index 가 GO/Closed deal 만이 아니라 **PASS 결정 (IC 검토 후 미투자) 도 first-class institutional memory** 로 보존. 센디 PASS (2026-02-15) 사유 = 구조 (10억서 AXE CB 외 equity 참여 제한) + 타이밍 — **quality/IRR 아님** (물류 round-trip 최적화 긍정 평가, "할 수 있었으면 좋았을 deal"). 7-deal portfolio = Closed 3 (Iippo/Sentry/Canopy) / IC 3 (Starnex/Hancom/Novachips) / Passed 1 (Sendy). **시사점**: (1) [D-index-19](/ops/decisions) 가 "surface vs reviewed gap" 였다면 본 결정은 "**computed vs manually-overridden gap**" — index 가 후자를 물리적 차단, (2) screening-grade model honesty — 92% 는 math-correct 이나 Exit EV 낙관 (sendyX 2026E 25억 = beta 5곳 월375만원 대비 66x 미검증); inputs_hash 가 가정 재현성 보존하되 가정 타당성 판단은 별도 (validate sanity 확장 여지), (3) declined deal 이 "왜 안 했는가 + 그 시점 IRR" 보존 → 센디 Q4 post-mortem (sendyX 상용화/BEP) 시 actual 대비 검증 가능. seed = `/Users/axe/index/seeds/sendy.yaml`. | 2026-05-29 |
| D-index-19 | **reviewed truth 축적 — index 는 calibrated 검토 본 저장, xlsx surface cell 아님** — 2026-05-28 사용자 요청 "3건 (스타넥스/노바칩스/한컴인스페이스) 찾아서 축적하되 그대로가 아닌 검토 + 시사점" 의 답. 3 pre-IPO deal 을 `_comparative_20260528_preIPO/` (13-agent 비교 IC 메모) 의 **common-protocol calibrated** 본으로 index 축적. **핵심 발견 — surface IRR ≠ reviewed IRR (최대 36pp gap)**: 스타넥스 surface(IC v3) 19.6% → calibrated 36.4% (+17pp), 한컴 surface 58.5%(P 거래소 85%) → calibrated 22.4% (-36pp, KOSDAQ 재청구 base rate 40~55% anchored 65% 로 down-cal), 노바칩스 surface -1.5% → calibrated 7.45% (+9pp). **만약 xlsx surface cell 을 그대로 축적했으면 한컴이 1위로 오기록** (실제 calibrated 랭킹 스타넥스 > 한컴 > 노바칩스) → index 의 본질 = typed reviewed truth 보존 (D-index-1 실증, raw cell dump 아님). **5 시사점**: (1) surface vs calibrated gap → index 가 검토 본 저장하는 가치 정량 실증, (2) **instrument structure 가 IRR-material (8~15pp)** — 노바칩스 보통주 구주 instrument-adjusted -4.55% vs surface 7.45% = 12pp; 현 schema `instrument` enum 은 있으나 `irr_instrument_adjusted` 자동계산 없음 → [B-index-instrument-adjusted-irr](/ops/backlog) gap, (3) probability calibration provenance (base rate anchor) 가 `scenario.description` 에 박제 → "왜 65%?" 추적 가능, (4) **동일 exit_matrix_leaf schema 가 Pre-A multi-leaf (Iippo/Sentry/Canopy) + pre-IPO single-leaf (각 scenario=단일 outcome) 양쪽 수용** — schema 유연성 검증; timing 이 bucket(3y/5y/7y) → 소수(2.5y/4.5y) 확장 필요 (parse_timing_years + CHECK 완화 migration 20260528000003), (5) **변곡점: IC 산출물 완성도 ≠ deal quality** — 노바칩스 (5 estimator × 8 scenario, 가장 정교) = 최약체 (IRR 7.8% 최하위 + Sharpe -0.02), index cross-deal benchmark 가 "polish bias" 객관 노출. **검증**: 6-deal portfolio IRR 랭킹 Sentry 40.3 > Iippo 36.5 > Starnex 36.4 > Canopy 26.4 > Hancom 23.2 > Novachips 7.8, pre-IPO 내부 랭킹 AXE 비교메모 결론과 일치, validate-model 6/6 PASS. seed = `/Users/axe/index/seeds/{starnex,hancom_inspace,novachips}.yaml`. | 2026-05-28 |
| D-index-18 | **DSL formula = "single driver + period-indexed values" 패턴 강제 (Y-suffix multi-driver 금지)** — 2026-05-28 사용자 질문 "Excel model → DB화 best practice" 의 답. 본 service 의 모든 financial_model 은 driver 를 시간축 분할 (`revenue_growth_y1`, `revenue_growth_y2`, ... `revenue_growth_y7` 처럼 7개 개별 driver) 하지 않고, **단일 driver (`revenue_growth`) + period 0..N 별 값** 으로 표현. **이유**: (a) DSL 의 `lag` 연산자 (`revenue[y-1]`) + `current_period` 자동 lookup 이 자연스럽게 작동 — formula `revenue[y-1] * (1 + revenue_growth)` 이 period 0 에는 growth[0], period 1 에는 growth[1] 을 자동 읽음. (b) Y-suffix anti-pattern 의 함정 박제 — compute_full_model (commit `9df7f71`) 의 Iippo base output: 모든 period 에 `revenue_growth_y1=6.5` 만 reuse → 7년차에 revenue 1.2 quadrillion 원 비현실적 compounding, seed comment "Y2+ override 별 row" 의도였으나 DSL 이 단일 formula 만 지원 → 모델 거짓 양성. (c) **Excel 의 "input row" 패턴과 동형** — Excel 에서도 growth rate 는 1 row × N column (period 별), 별도 7 row 가 아님. **현재 seed 정정 필요**: Iippo 의 `revenue_growth_y1~y7` 7 개 driver → 단일 `revenue_growth` driver + period 0..6 별 driver_value rows. Sentry/Canopy 는 exit-matrix-only 라 영향 없음. `revenue` formula 도 단순화 (`revenue[y-1] * (1 + revenue_growth)` 로 충분). 본 결정으로 도입되는 backlog: [B-index-driver-period-indexed-refactor](/ops/backlog) (Iippo seed 정정 + re-ingest + compute_full_model 재검증 + IRR ±1pp 회귀). **best practice 결정 4종** ([D-index-15](/ops/decisions) frozen enum 의 modeling 차원 확장): (1) driver code = snake_case 명사, 시간축 suffix 금지, (2) period-varying value 는 driver_value rows (period_index 컬럼), (3) formula 는 단일 표현 — period-별 분기 금지 (해당 시 lag/multi-period 패턴 사용), (4) base_revenue_period0 같은 sentinel 은 financial_model row 의 명시적 컬럼. **LP-deliverable 관점**: 동일 model 을 1년 후 재계산해도 deterministic — driver_value 의 period 별 값이 audit trail 의 단일 SoT. Excel 의 `=A2*(1+$B$3)` 같은 cell-reference anti-pattern 영구 차단. | 2026-05-28 |
| D-index-17 | **ic + ingest skill ownership Blueprint → index 이전 (overlap 기간 양쪽 보유)** — 2026-05-28 사용자 결정. 도메인 alignment: `ic` skill (19-agent IC memo orchestration) 은 투자 도메인 → index 가 자연 owner. `ingest` skill (dataroom pdf/pptx/xlsx → md clone) 은 ic 의 직접 의존이라 함께 이전. **이전 방식 = overlap (copy)**, symlink 아님 — 안정화 기간 동안 양쪽 모두 byte-identical 보유, **index/skills/ = canonical SoT**, `/Users/axe/blueprint/.claude/skills/` = mirror (deploy 호환성). Phase 1 후속 대상: `due-diligence`, `vc-deal-sourcing` 도 도메인 정합 (별도 결정). `portfolio-management` + `investor-relations` 는 [D-index-12](/ops/decisions) 에서 이미 pmc skill 흡수 결정. `ctx` / `humanizer` / `report-writing` / `vault-secret-capture` / `office-admin` / `code-review` 등 일반 skill 은 Blueprint 잔존. **안정화 sunset 기준 3가지** (모두 충족 시 Blueprint mirror 제거 — 별도 결정): (a) 다음 3 deal 의 IC 실행이 `index/skills/ic/` 기반 정상 작동, (b) Blueprint Claude Agent SDK 가 index/skills/ mount/sync 자동화 확립, (c) `ic --push-to-index` mode 일반화 ([B-ic-push-mode-impl](/ops/backlog)). 본 결정 + sunset 작업 = `B-index-skill-overlap-sunset`. **Phase 2 진화 path** (별 backlog): `B-index-skill-mcp-discovery` (index MCP 에 `list_skills` + `get_skill_resource` tool) + `B-blueprint-skill-mcp-client` (Claude Agent SDK 가 MCP 로 skill 발견, filesystem 의존 제거) + `B-index-skill-versioning-schema` (`index.skill_resource@1.0` schema + audit_event — per-deal "이 IC 의 메모는 ic skill v8.3 으로 작성" trace). Overlap 기간 sync 책임 = index 편집 후 Blueprint mirror 로 sha256 검증 cp. Blueprint hook (`skill-sot-guard.sh`) 가 main 직접 편집 차단 → drift 방지 자동화. 상세 = [/services/index/skill-ownership](/services/index/skill-ownership). | 2026-05-28 |
| D-index-16 | **IRR methodology 단일화 — per-leaf weighted 만 사용, anchor methodology deprecate** — 2026-05-28 사용자 결정. 본 service 의 모든 deal IRR 산출은 **per-leaf weighted methodology** (each leaf: `(proceeds/committed)^(1/timing) - 1` 산출 후 joint_probability 가중 합산) 로 통일. 별 anchor methodology (V5 final 의 F/D Pre anchor + Series A 희석 chain + success_probability 단일 flow) 폐기. 이유: (a) **데이터 정합으로 충분** — V1 yaml outdated → V5 final 의 3 보강 layer (광고 element 본문화 + 서울시 4자 합의 + KB 캐피탈 capex debt backbone) 반영 시 per-leaf 가 자연스럽게 27% 매칭. Canopy V1 → V5 정합 3-step (writedown recovery_rate 도입 + scenario probability rebalance + never writedown 비합리성 정정) 으로 -17.41% → 26.39% 달성, IC pass 27% baseline Δ=0.01 PASS. (b) **단일 methodology 운영 단순성** — deal 마다 다른 methodology 선택 logic 불필요. financial_model schema 에 `irr_methodology` enum 추가 불필요. (c) **3 deal per-leaf ALL ±1pp**: Iippo 37% (computed 36.52%) / Sentry 41% (40.26%) / Canopy 27% (26.39%) — 일관성 검증 완료. 본 결정으로 폐기되는 backlog: `B-index-canopy-anchor-irr-algo` (anchor IRR 별 algorithm) + `B-index-canopy-v5-anchor-methodology` (anchor 재구현). 본 결정으로 발견된 신규 발견 = V1 yaml 의 outdated assumption (upside scenario 10% never writedown, base 20% never writedown 같은 구조적 결함) 을 ic skill `--push-to-index` 시 자동 surface 가능 — within-scenario writedown probability 합리성 check 가 financial_model validate trigger 로 추가 ([B-index-leaf-probability-sanity](/ops/backlog) 신규). `src/irr.rs` 의 `anchor_irr` + `canopy_anchor_grid` 는 `#[deprecated]` 표시 후 Phase 1 에서 삭제. | 2026-05-28 |
| D-index-15 | **Schema authority immutable — DSL grammar + enum 은 service code 에만, skill 쪽 override 불가** — index 의 `financial_driver.formula` DSL grammar (10 operator: + - * / min max if sum avg lag growth) + `risk_alert.kind` enum 5종 + `instrument` enum 5종 (RCPS/BW/SAFE/신주/보통주) + `fund_investment.status` enum 6종 + `exit_matrix_leaf.path` enum 6종 (ma/ma_strategic/ipo/secondary/writedown/partial) 모두 **service code 에 hardcoded**, `customers.yaml` / 환경 변수 / runtime config 으로 override 불가. ic skill 의 `scenario_deltas.yaml` 이 DSL syntax error 또는 unknown enum 값 emit 시 `ingest_financial_model_xlsx` 가 `error.code = "formula_syntax_error"` 또는 `"unknown_enum"` + suggestion 본문 반환 → ctx review queue 에 highlighted error + 정정 예시 표시 → 사용자 수정 후 confirm. **skill schema drift → service crash** 시나리오 영구 차단 (worst-case risk #1). 새 enum value 추가는 (a) `src/index/schemas.rs` SoT 수정 + (b) `/index/schemas` envelope `@1.0` → `@2.0` version bump + (c) Blueprint `artifact_schema` 자동 mirror + (d) ic/pmc skill 의 yaml validator 갱신 — coordination PR 강제 (silent 추가 금지). frame 의 KSME `accounting_standard` enum 영구 freeze 패턴 ([D-frame-fund-ksme-policy-check](/ops/decisions)) 의 index domain 확장. | 2026-05-27 |
| D-bp-entity-canonical | **Entity canonical SoT 의 계층화 — Blueprint 가 entity 의 canonical name + customer 소속 + role SoT, 도메인 메타는 도메인 service SoT, customers.yaml 은 deployment 식별 SoT** — 운영자 질문 ("id, entity 정보는 blueprint 가 SoT 가 되어야 할 것 같습니다") 을 architectural 결정으로 정착. 현재 entity 정보 4 곳 분산 (customers.yaml.entities + customers.yaml.entity_meta + frame.shared.entity + Blueprint EntityRole + Blueprint User.entityScopes) — drift 위험 + bus factor. **계층화된 SoT**: (a) `customers.yaml.entities` = customer→entity 식별 (deployment prerequisite, 부팅 self-contained, 변경 X), (b) **Blueprint `Entity` 테이블 신설** = entity 의 canonical name (한글) + customerId + role (corporate/GP/etc) + isActive — 사용자-facing + 인증 게이트 + UI selector 의 자연스러운 위치, (c) `frame.shared.entity` = 회계 도메인 메타 (accounting_standard / fund_meta / biz_no / entity_kind) 유지, (d) Blueprint `EntityRole` + `User.entityScopes` = 권한 (기존 SoT 유지). 본 2026-05-27 세션의 `customers.yaml.entity_meta` 신설은 **transitional** — Blueprint Entity 테이블 신설 후 derived view 또는 cross-check drift 검증으로 전환 ([B-customers-yaml-entity-meta-derive](/ops/backlog)). **단계화 3-phase**: Phase 0 (본 결정 등재 + backlog 2 항목 등재) / Phase 1 ([B-bp-entity-table-create](/ops/backlog), Prisma Entity model + customers.yaml + frame.shared.entity union seed) / Phase 2 (customers.yaml.entity_meta 를 axe CLI 의 derived view + matrix collector 에 drift 검증 추가). frame/hive 의 `legal_name` 영문 → 한글 정정 ([B-frame-entity-legal-name-i18n](/ops/backlog)) 도 Blueprint Entity SoT sync 의 일환. | 2026-05-27 |
| D-ui-1 | **통합 layout chrome `.axe-app-shell` 도입 + `ui.axelabs.ai` 디자인 시스템 사이트 신설** (Phase 15) — cortex 운영자의 "docs.axelabs.ai 와 묘하게 다르다" 지적 (2026-05-29) 을 출발점으로, 토큰만 SSOT 화하던 기존 상태를 chrome 자체 SSOT 로 확장. **3 결정**: (a) **variant 옵션** — 단일 `.axe-app-shell` 이 `data-shell="docs|dashboard|landing"` 3 형태 분기 (sidebar+content+toc / sidebar+content / single-col), `--app-shell-{sidebar-w,toc-w,gutter,topbar-h}` 4 변수 노출. (b) **CSS-only primitives + thin React wrapper** — Rust(cortex maud)/Python(jinja) 모두 동일 markup 으로 가져갈 수 있게 styling 100% CSS 클래스 + data attribute. React `&lt;AppShell&gt;` 는 슬롯 합성 + semantic role 만. (c) **Nextra theme override** — docs.axelabs.ai 폐기 비용 큼, navbar/sidebar 슬롯에 axe 컴포넌트만 끼움 (C2 가성비 모델). **도메인 = `ui.axelabs.ai`** (1-level subdomain) — 짧음, `@axe/ui` 패키지 이름 일치, `docs.axelabs.ai` 와 페어. **배포 모델 = axelabs.ai Next.js multi-domain rewrite** (a) — host header 분기 (`Host: ui.axelabs.ai → /design/*`), 별도 컨테이너 X, build 산출물 공유. axelabs.ai/design 직접 접근도 살아있음 (dev/preview/canary). Phase 1 완료 (2026-05-29): `src/lib/styles/layout.css` 의 `.axe-app-shell*` primitives (200+ lines) + `src/lib/components/app-shell/{AppShell.tsx, Toc.tsx}` + `app/design/{page.tsx, shell/page.tsx}` 데모 + `.claude/launch.json` `axe-ui-design-preview` (:3902, :3900 은 docker production 점유) + `next.config.mjs` rewrites. 3 variant + dark mode 모두 브라우저 검증. **순차 적용 (운영자 확정 후)**: (1) ui.axelabs.ai 인프라 (DNS + tunnel ingress + customers.yaml) (2) cortex `scripts/sync-axe-ui.sh` 갱신 + maud `page()` AppShell markup (3) docs.axelabs.ai Nextra theme override (4) axelabs.ai 메인 `/app/page.tsx` → `variant="landing"` (5) frame · hive · matrix 신규 적용. **본문**: [/services/ui](/services/ui). | 2026-05-29 |
| D-ui-2 | **layout primitives (Stack/Cluster/Grid/Container) + ThemeToggle 를 @axe/ui SSOT 로 흡수** (Phase 16) — [D-ui-1](/ops/decisions) chrome SSOT 의 micro 레이아웃 짝. 출발점: matrix 페이지가 `col(gap)`/`grid()` inline 헬퍼로 flex/grid 를 재발명 + design 페이지가 ThemeProvider 우회해 `document.documentElement` 를 직접 토글하던 버그. **흡수 5종**: Stack (세로 flex+gap) · Cluster (가로 flex+wrap — toolbar/액션 줄) · Grid (`auto`+`min` auto-fill 으로 cortex `.cortex-metric-grid` 대체) · Container (max-width 중앙) · ThemeToggle (light/system/dark, `useTheme` 위 thin UI — DOM/storage 반영을 Provider 에 위임, 외부 아이콘 dep 0). chrome 과 동일하게 styling 100% CSS 클래스 (`.axe-stack`/`.axe-cluster`/`.axe-grid`/`.axe-container`/`.axe-theme-toggle` + `--axe-grid-min`) → cortex maud/jinja 등 비-React 소비자 동일 markup. gap = spacing scale 1:1 (매직 px 금지). **채택·브라우저 검증 2026-05-29**: `app/page.tsx` (axelabs.ai 홈 — Hero/Section/Footer 컴포지트 + Stack/Cluster/Grid, D-ui-1 step 4 동시 이행) + `app/matrix` (MatrixNav ThemeToggle + page 전체) + `app/design` (ui.axelabs.ai). cortex/docs/blueprint 순차 적용 대기. 본문: [/services/ui § Layout primitives](/services/ui). | 2026-05-29 |
| D-ui-3 | **디자인 시스템 도메인 `ui.axelabs.ai` → `design.axelabs.ai` rename (완전 교체·구 도메인 폐기) + 문서 HTML 템플릿 (IC Memo · LP letter, 가로/세로) 도입** — 운영자 Phase 2 결정 (2026-05-29). **(1) 도메인**: `design.axelabs.ai` 가 의미를 더 명확히 전달 (디자인 시스템 + 문서 템플릿 showcase). `ui.axelabs.ai` 는 ingress 제거로 catch-all 404 (완전 교체, 병행 X). 패키지 이름은 `@axe/ui` 유지 (도메인 ≠ 패키지) → docs 경로도 `/services/ui` 유지. **(2) 분기 메커니즘 정정**: host 분기는 `next.config.mjs` `rewrites()` 가 아니라 **`proxy.ts`** (Next.js 16 convention) — 정적 prerender 된 `/` 에 `next.config` rewrites 가 안 걸리는 함정 우회 (proxy 는 항상 runtime). `Host: design.axelabs.ai` → root 를 `/design` rewrite, sub-path 는 root 로 308, `axelabs.ai/design/*` 는 404. **(3) 인프라 (vault CF 토큰으로 처리)**: `design.axelabs.ai` proxied CNAME → `d8efecdd….cfargotunnel.com` 을 axelabs.ai zone 에 CF API (`Cloudflare API - axelabs` 토큰) 로 생성. tunnel ingress 는 **remote-managed** ([known-gaps Cortex 7함정 #7](/ops/known-gaps)) 라 `config.yml` 편집 무효 → `PUT /accounts/<acct>/cfd_tunnel/<uuid>/configurations` 로 `ui`→`design` 1-rule swap (v21→v22, 나머지 15 rule 보존). 함정 2개: ① `cloudflared tunnel route dns` 는 cert-scoped (axellc.com) 라 `design.axelabs.ai.axellc.com` 오생성 → CF API 로 삭제, axelabs.ai zone DNS 는 반드시 API 토큰 사용. ② 신규 1-level host 직후 로컬 resolver AAAA-only → IPv6 무라우팅 머신 curl HTTP 000 (production 무관, `curl -4`/`--resolve` 로 검증). 검증: design 200 (양 edge·@axe/ui 페이지) · sub-path 308 · ui 404 · axelabs.ai/www/docs 무영향. **(4) 문서 템플릿**: AXE 발신 문서 (IC Memo · LP letter 등) 를 `.axe-doc` CSS-class HTML 로 — 가로(landscape)·세로(portrait) **모두** modifier 로 지원 (문서 성격별 고정 X), A4 `@page` + `@media print` (브라우저 인쇄→PDF), 기존 `md-to-pdf` 렌더 파이프라인과 통합 (병렬 렌더러 신설 X). design.axelabs.ai showcase 에 orientation 토글 + 인쇄 버튼. **구현 완료 (2026-05-29)**: `src/lib/styles/document.css` (`.axe-doc*` + named `@page` 스코프) + `src/lib/templates/documents/ic-memo.html.jinja`·`lp-letter.html.jinja` (jinja2 가로·세로 4 combo 렌더 검증) + `app/design` 쇼케이스 섹션 (문서·orientation·zoom 토글 + 인쇄). 렌더는 기존 md-to-pdf skill (pandoc + chromium `--print-to-pdf`) + ic skill `check_pdf_quality.py` 게이트 재사용 (병렬 렌더러 X — body 슬롯에 markdown→HTML 주입). **본문**: [/services/ui](/services/ui). | 2026-05-29 |

### D-bp-mcp-1 — Blueprint MCP standalone (양파껍질 lesson)

Blueprint 자신의 read-only MCP 서버 launch (`axe.axelabs.ai/blueprint/mcp`, 9 tools). 의도: frame 의 `auth_oidc.py` + `mcp/http_server.py` 를 byte-by-byte 미러. 실제: "비슷하게" 작성 → claude.ai 등록 시 **5 단계 순차 차단** (PR #331~#335, 약 1.5 h 소비):

| # | 차단 | 메시지 (claude.ai) | 원인 |
|---|---|---|---|
| 1 | server 500 | "integration may not be available right now" | `httpx.Timeout(connect, read)` 2-arg → httpx 0.28 ValueError |
| 2 | 진단 불가 | (모든 401 이 같은 메시지) | `_unauthorized` body 가 reason 무시 |
| 3 | 토큰 검증 401 | "rejected the credentials, so the connection was reverted" | `audience=app_id_uri` 단일 (v2 토큰 aud = client_id GUID 임) |
| 4 | 인증 후 첫 호출 RuntimeError | "Task group not initialized" | Starlette Mount 가 FastMCP child lifespan 미전파 |
| 5 | 421 Misdirected | "Invalid Host header: axe.axelabs.ai" | FastMCP DNS-rebinding guard, allowed_hosts env 미설정 |

**결정**: 새 MCP 서버는 [/architecture/mcp-server-checklist](../architecture/mcp-server-checklist) 의 9·5·5·14 체크포인트 (azure manifest 9 · python 코드 5 · compose 5 · 운영 14) 를 1:1 통과 후 production. 시작은 `cp -R frame/src/frame /tmp/NEW_SERVICE` 부터, 그 다음 service-specific edits.

### D-bp-mcp-2 — Blueprint MCP blue/green pair

D-bp-mcp-1 launch 직후 진단으로 발견: Blueprint MCP 는 frame/hive 의 docker compose 패턴 (`&frame-mcp-blue` anchor + green sibling) 을 **미러하지 않은 단일 replica** 였음. `docker compose up -d --build mcp` 시 build + recreate 5-10s 다운 발생 — D-config-13 의 "다운타임 0 frame deploy" 원칙에 어긋남.

**결정**: `docker/docker-compose.yml` 의 `mcp:` 블록을 `mcp-blue: &blueprint-mcp-blue` + `mcp-green` 으로 분리. blue 가 docker network alias `blueprint-mcp` 보유 (active), green 은 alias 없이 가동 (passive). swap = `docker network connect/disconnect --alias` + `caddy reload`, sub-second. cloudflared 비건드림 (D-config-13 §3 frame 패턴과 동일).

host port: `blueprint-mcp-blue:3152`, `blueprint-mcp-green:3153` (디버그용, 외부 path 는 여전히 `:3151` Caddy proxy).

### D-bp-entity-1 — Blueprint entity 개념 도입 (PARA × entity)

진단 (2026-05-21 5분야 멀티 에이전트): Blueprint 의 `Organization` 모델이 single-row stub (`prisma/schema.prisma:689`), `Workspace/UsageLog/Agent` 에 organization FK 부재 (line 688 "별도 PR" 주석). 동시 `customers.yaml` 의 `axe.entities: ["axec","axev"]` (frame/hive 와 정합) 은 이미 회계/HR 차원에서 격리 운영 중. Blueprint 의 PARA 폴더 컨벤션 (`/Ventures · /Corporation · /General` × `1. Project · 2. Area · 3. Resource · 4. Archive`) 도 entity 차원의 격리를 함의.

**결정**:

| 항목 | 방향 |
|---|---|
| entity 정의 | `axec` (AXE Corporation), `axev` (AXE Ventures) — frame/hive 와 정합. `General` 은 cross-entity bucket 으로 추후 (axec/axev 외) entityId 부재 row 가 대응 |
| 모델 | `Workspace.entityId` FK (nullable for migration) + `Workspace.paraLayer ParaLayer?` enum `{PROJECT, AREA, RESOURCE, ARCHIVE}` — 4 PARA layer 모두 동일 row-level entity scope |
| 격리 강도 | hive 의 schema-per-entity 와 달리 **row-level FK + auth gate** (D-hive-1 의 SQL boundary 격리 미적용). Blueprint 의 cross-entity workflow (Teams bot · IC pipeline · Trinity) 와 부합 |
| Area/Resource/Archive | 별도 first-class 모델 안 만들고 Workspace 의 paraLayer variant. 고유 속성 발견 시 후속 PR 분리 |
| 적용 시점 | **Stage 1 외부 customer rollout 전**. `customers.yaml` 의 `axe.entities` 가 이미 production 이므로 single-customer 내 multi-entity 만으로도 시급 |

**Implementation** ([PR #339](https://github.com/soohunkang/blueprint/pull/339), 2026-05-22, 단일 PR 4 commit):

1. Prisma migration: `Entity` 모델 + `ParaLayer` enum + `Workspace.entityId/paraLayer` + D-bp-entity-2 provenance 3필드 + `User.entityScopes`. Idempotent SQL + axec/axev seed. Postgres baseline 부재 → `scripts/apply-entity-para-migration.ts` 운영자 1회 적용 후 매 boot `seed-entities.ts` 가 idempotent upsert.
2. drivePath 기반 backfill (`scripts/backfill-workspace-entity-para.ts`): `/Ventures/...` → entityId=axev, `/Corporation/...` → entityId=axec, `paraLayer` = `1_Project`/`2_Area`/`3_Resource`/`4_Archive` segment match. dry-run + apply 분리.
3. `src/lib/entity-scopes.ts` (resolve/hydrate/get/require + entityScopeWhereFilter) + `src/lib/auth.ts` signIn hydration + `src/lib/workspace.ts` listWorkspaces opt 확장 + `/api/workspaces/*` 게이트.
4. UI: `/axe/projects` entity dropdown + 항상 `paraLayer=PROJECT` fetch / `/axe/ara` stub → ARAClient 3-tab + fork badge.

**사용자 결정 (2026-05-22)**: Workspace ↔ Entity = scalar 1:N (OneDrive `/Ventures/` `/Corporation/` 폴더 분리가 SOT, cross-entity 는 `User.entityScopes` 로 처리). frame fund 회계 N:M 권고는 reject.

**Production hotfix** ([PR #341](https://github.com/soohunkang/blueprint/pull/341), 2026-05-22): verification 중 발견한 3 drift — (a) `docker-compose.yml` app-green block 에 customers.yaml mount + `CUSTOMERS_YAML_PATH` env 누락 (blue 만 있던 mount 가 cutover 시 옮겨지지 않아 entityScopes hydration 항상 admin-fallback), (b) `scripts/apply-entity-para-migration.ts` 의 header comment 안 `$$` 가 splitter inDollar flag 오염 → P2010, (c) backfill regex `[.\s]` 에 underscore 추가 (`1_Project` 매치).

**Production 결과**: Entity 2 row (axec/axev seed) · Workspace 18 row 중 16 backfill 적용 (PROJECT × axec: 1 / PROJECT × axev: 14 / PROJECT × null: 2 / AREA × axev: 1 [HR]) · 5 user entityScopes hydrate · Chrome MCP /axe/projects + /axe/ara 검증 PASS.

### D-bp-entity-2 — PARA dispatch flow sub-consensus

D-bp-entity-1 머지 후 Project 종결 → Area/Resource 이관 (dispatch) 흐름. 본 결정은 dispatch UI ([PR #339](https://github.com/soohunkang/blueprint/pull/339) 의 schema 까지만 포함, UI 는 PR 5 예정) 의 4 합의:

| # | 합의 |
|---|---|
| 1 | **Copy-with-provenance (Fork)** — Project row → Area/Resource row 는 dispatch 시점의 snapshot copy. 원본 archived workspace 에 freeze. `sourceWorkspaceId` / `sourceArtifactPath` / `copiedAt` 3 필드 (D-bp-entity-1 schema 에 통합) |
| 2 | **검색 분리** — Archive = "그때 어떻게 생각했는지" (시점형, 의사결정 archaeology). Area/Resource = "지금 이렇게 생각한다" (현재형, living knowledge). Search surface 가 PARA-aware |
| 3 | **끊임없는 정비 workflow** — Dispatch 는 일회성 migration tool 이 아님. 복사본은 각 Area/Resource 의 목적에 맞게 지속 편집되는 living document. PARA 철학 (Tiago Forte) 핵심 |
| 4 | **자동 분배 ≡ LLM 제안 + 사용자 확인** — NEVER fire-and-forget. Suggestion-based, non-destructive |

**미해결** (dispatch UI PR 직전 결정): (a) Dispatch unit (file vs semantic chunk), (b) Area instance 정의 권한 (org-admin vs free), (c) 정비 트리거 우선순위 (manual / periodic / LLM-detected staleness). **권고 경로**: Path B Spike (DB 변경 없이 단일 workspace 로 흐름 검증 3-5일) → Path A 본구현 (Area/Resource UI + dispatch modal + archive search separation).

**관련 진단**: `/Users/axe/.claude/projects/-Users-axe-blueprint/memory/feedback_multi_agent_diagnosis_2026_05_21.md`

### D-bp-para-1 — PARA 범용 재구조화 (조직론 + 집(link) 모델 + governance + 죽은 딜 본질)

[D-bp-entity-1](#)/[D-bp-entity-2](#d-bp-entity-2--para-dispatch-flow-sub-consensus) 의 AXE-shaped 초기 설계(copy-with-provenance · Area=workspace)를 **범용 AI OS 관점으로 재구조화** (2026-06-06 설계 세션). 상세·철학 = [/architecture/para-os](/architecture/para-os). 미구현 dispatch/UX zone 한정 — 저장층(Artifact/ArtifactLink/McpSchema, [PR #339](https://github.com/soohunkang/blueprint/pull/339)) schema 는 유효.

**확정 결정**:

1. **PARA = 조직론** — Area=영속 기능(정적 조직, MCP=물질화), Project=cross-functional TFT(동적 조직). Resource·Archive 는 *각 Area 안*, Project 만 가로지름.
2. **dispatch = "집(home)" 모델** — move/copy/link 3지선다는 폴더-thinking 증상. artifact 몸통 1 + 집 N(link). **link=기본**(다중 Area 참조), **move**=소모성 working→Archive→폐기, **copy-curate**=재사용본 저작→Resource. → D-bp-entity-2 의 **copy-with-provenance default 를 link-default 로 revise** (copy 는 method→Resource 큐레이션 예외만).
3. **Blueprint = 도메인 서비스가 *안 덮는* 것의 substrate** (미졸업·무서비스 Area live + 그 R/A + cross-service 종합물 + 가로 결정로그 mirror). 졸업 서비스(frame/hive/index/gate)는 **자기 도메인 지식(죽은 딜·섹터 프레임 등 R/A 포함)을 가져감** — moat(죽은 딜 복리) = index. Area 졸업 트리거 = 도메인 로직/AI 상호작용(저장 아님). _[정정 2026-06-06: 원안 "모든 R/A → Blueprint" 를 졸업 기준으로 좁힘 (사용자 확정), 상세 [para-os §4](/architecture/para-os)]_
4. **substrate 3분** — typed Area=쿼리 / Resource=SoR+벡터 index(hot) / Archive=cold·폐기가능(추출 지식만 영구·queryable). 벡터=저장소 아닌 index.
5. **Governance 분리** — 결정로그(record: Blueprint core, append-only, supersede, 모든 Area cite) vs 결재워크플로(process: 전자결재, policy-driven·agent-assisted layer). 서명 한 종류(내부=외부 e-sign). Area 서비스=결정 actuator. 통합 구현=[B-bp-decision-pipeline-esign](/ops/backlog).
6. **본질 = 죽은 딜 저장·harvest** — VC 지식의 대부분이 dead deal(judgment "왜 패스" + 섹터지식)에. Archive raw=폐기 가능하되 추출 지식=영구. index 가 passed/dead 딜을 1급 저장해야 하는 근거.

**열린 질문** (확정 시 후속 D 등재): (a) 2-level scope(personal+shared), (b) governance servicization(Blueprint core vs 독립 서비스 — 부족함 진단 저장/로직/AI 로 가름), (c) Project staffing, (d) Rust 적용 범위(기존 Python 서비스 확장 언어).

**제약**: 신규 코드 = Rust (사용자 지침). SoT 메모 = `project_para_ai_os_philosophy.md` (Blueprint global memory).

### D-bp-rust-1 — Blueprint 점진 Rust 전환 (strangler-fig) · substrate = 첫 organ

[D-bp-para-1](/ops/decisions) 의 **열린 질문 (a)/(c)/(d) 를 확정**하고, "신규 코드 = Rust" 지침을 Blueprint 토폴로지에 정착 (2026-06-06 세션). 사용자 발화: "점차 blueprint 도 rust 로 전환할 계획입니다 … 점진적 대체로 하려고 합니다."

**전략 — strangler-fig (점진 대체)**:

1. **프론트엔드(React/Next.js UI) = 당분간 TS 유지.** "Blueprint→Rust" = *백엔드* 전환이지 UI 재작성이 아님. WASM 프론트(Leptos/Dioxus) 전면 재작성은 별개·먼 결정 (현재 scope 아님). → 열린질문 (d) 확정: **Rust 범위 = 신규 백엔드 organ, 프론트 제외**.
2. **백엔드(API routes + `src/lib/*` 로직) = 도메인별 Rust(axum) organ 으로 점진 추출.** Next.js 는 점점 얇은 frontend-of-record 로 후퇴, Rust 를 내부 HTTP 호출.
3. **데이터 공존 = Postgres schema-split 소유권** ("두 ORM 한 DB" 를 안전하게): Prisma=`public`(Workspace/Entity/User/Session/Issue…), Rust/sqlx=`substrate`(artifact/artifact_link/mcp_schema). 한 blueprint-postgres 인스턴스, **교차쓰기 금지** (substrate 는 Workspace 미접근, Prisma 는 artifact 미접근 → 마이그레이션 도구 충돌 0). `public` 이 시간에 따라 줄어드는 게 strangler.

→ [D-bp-artifact-4](/ops/decisions) **refine (모순 아님)**: "monolith first / 별도 KnowledgeStore service 분리 안 함 / 신규 인프라 0" 의 *인프라* 정신은 보존 (동일 blueprint-postgres · 동일 compose · 동일 `axe ship blueprint`). 바뀐 것은 *언어* 뿐 — knowledge substrate 를 Rust 프로세스로 구현 (언어 마이그레이션 목적의 프로세스 분리). frame/hive/gate 식 **독립 product-service 가 아님** (cloudflared·공개 MCP·blue/green pair·customers.yaml service·pre-push guard 등재 전부 없음).

**첫 organ = PARA substrate**:

- **경계 = Artifact-scoped** (사용자 확정): substrate 가 `artifact` / `artifact_link`(+`annotation`) / `mcp_schema` 소유. `workspace_id` · `entity_id` 는 OneDrive citation 처럼 **opaque 외부 ref**(FK 없음, 데이터 중복 0). **Workspace + paraLayer SoR = Blueprint/Prisma 잔류**. Workspace lifecycle 변경(reclassify/close) → 내부 신호(pg_notify 또는 webhook)로 substrate 의 cached paraLayer 동기화 ([D-bp-artifact-6](/ops/decisions) "Re-sync on paraLayer change" 의 cross-process 판).
- **모델은 처음부터 옳게**: [D-bp-para-1](/ops/decisions) home/link("집") 1급 — `artifact_link (artifact_id, workspace_id)` = home 멤버십 본체 + **`annotation`**(Area 별 해석 레이어), dispatch mode `link`(기본)/`copy_curate`(method→Resource)/`move`(working→Archive), `parent_artifact_id` → `derived_from_id`(진짜 파생 lineage 만). **2-level scope(personal/shared)** = artifact `scope` 컬럼 → 열린질문 (a) 확정. citation = [D-bp-artifact-3](/ops/decisions) 6종 + [D-index-6](/ops/decisions) `index.*` + [D-gate-2](/ops/decisions) `gate.decision` = **8종** 처음부터.
- **배포 = blueprint compose 내 sidecar** `blueprint-substrate` (Rust+axum, 내부 포트). 위치 = `blueprint/substrate/` (레포 내부 Cargo — polyglot 레포 = 점진 전환의 정상태). `axe ship blueprint` 한 경로 안에서 빌드.
- **Next→Rust 인증** = 기존 `BLUEPRINT_INTERNAL_API_KEY` Bearer (`/api/internal/*` 패턴) + caller identity·entity scope 헤더. 동일 compose 망 내부 호출 신뢰.
- **첫 organ 이 재사용 패턴 확립** (schema-split · 내부 auth 브릿지 · typed TS client · compose-service) → 이후 organ 들이 복제.

**열린 질문 처리** ([D-bp-para-1](/ops/decisions)):
- (a) 2-level scope → **확정: personal + shared** (artifact `scope` 컬럼, 기본 personal → 승급 shared).
- (b) governance servicization → 별 트랙 `gate` 서비스가 흡수 ([D-gate-1](/ops/decisions)/[D-gate-2](/ops/decisions)). substrate 와 무관.
- (c) Project staffing("던지면 emerge") → **후속 defer** (이번 substrate scope 아님; Project=컨테이너 + 기존 WorkspaceMember).
- (d) Rust 범위 → **확정: 신규 백엔드 organ = Rust, 프론트 = TS 유지, 점진 대체**.

**시퀀싱 (중요)**: 첫 scaffold 는 기존 M6 Stage-1 TS Artifact 테이블(Prisma `public`)을 **건드리지 않고 병행 신설**. cutover(Prisma Artifact 제거 + `/api/artifact/propose` → substrate client repoint)는 Rust 경로 동작 검증 *후* 별 stage — 기존 propose route 안 깨짐.

**정직한 비용**: polyglot 레포 + 두 마이그레이션 도구(schema-split 로 관리), Workspace↔artifact 교차 트랜잭션 상실(Artifact-scoped 라 수용 — workspace 생성 후 artifact propose 분리). 상세 ADR = `docs/adr/blueprint-rust-migration.md`. SoT 메모 = `project_para_ai_os_philosophy.md`.

### D-bp-ui-1 — Blueprint `@axe/ui` 채택 + Tailwind 4 패치

D-axe-ui-1 (SSOT 단일 배포 채널 결정) 의 Blueprint 측 implementation. 첫 SSOT 소비자 중 Tailwind 4 + Turbopack 스택을 가진 곳 — Nextra (axelabs-docs) 와 다른 CSS 파이프라인 거동 때문에 sync 단계 패치 필요.

**채택 범위 (2026-05-22)**:

| 항목 | 결정 |
|---|---|
| SSOT 경로 | `/Users/axe/axelabs/src/lib/{tokens,styles}/` (D-axe-ui-1 과 동일) |
| Blueprint dest | `src/app/_axe-ui/` (canonical `app/_axe-ui/` 와 다름 — src/ source root) |
| sync 스크립트 | `scripts/sync-axe-ui.mjs` — canonical `/Users/axe/axelabs-docs/scripts/sync-axe-ui.mjs` 패턴 + Blueprint 패치 |
| 자동화 | `predev` / `prebuild` npm hook + `check-axe-ui` dry-run |
| VERSION 필드 | `source-sha` / `source-lib-sha` / `source-lib-date` (`execSync git rev-parse + log -1`) |
| DOM 토글 마커 | `<html data-theme="dark">` 기본. `class="light|dark"` 도 SSOT 가 지원 (next-themes 호환) |
| CSP 추가 | `style-src` + `font-src` 에 `cdn.jsdelivr.net` (Pretendard + D2Coding CDN) |

**Blueprint 전용 패치 1 — fonts.css `@import url()` strip**:

axelabs SSOT 의 `src/lib/tokens/fonts.css` 는 Pretendard + D2Coding 을 `@import url("https://cdn.jsdelivr.net/...")` 로 로드. 이는 Nextra (axelabs-docs) 에서 정상 동작. 그러나 Blueprint 의 **Tailwind 4 + Turbopack 파이프라인** 은 `globals.css` 가 import 한 4 토큰 파일 (`colors.css` · `spacing.css` · `typography.css` · `fonts.css`) 을 globals.css 본문 안으로 **inline** 한 뒤 컴파일. 결과 CSS 의 구조:

```
[colors.css :root 블록] ...
[spacing.css :root 블록] ...
[typography.css body { ... } html { ... }] ...
[fonts.css 의 @import url(pretendard) ← 다른 rule 뒤로 밀림!]
[fonts.css @font-face { Sarasa } :root { --font-* }]
```

CSS spec: `@import` 은 `@charset` / `@layer` 만 선행 가능. 다른 rule 뒤의 `@import` 는 **브라우저가 silent drop**. 결과: Pretendard 미로드, body 폰트가 system sans-serif 로 fallback (검증 안 했으면 모르고 지나갈 수 있음). 빌드 단계에서 `@import rules must precede all rules aside from @charset and @layer statements` 경고 발생.

**회피**: SSOT 미변경. sync 스크립트가 `fonts.css` 복사 시 `content.replace(/^@import url\([^)]*\);\s*\n/gm, "")` 로 `@import url(...)` 두 줄만 strip. Pretendard + D2Coding 은 `src/app/layout.tsx` 의 `<link rel="stylesheet">` 로 로드 — 이는 SSOT 자체가 Clash Display 에 대해 fonts.css 주석에서 명시한 회피 패턴 ("Fontshare URL 의 f[]= 대괄호가 Next.js CSS @import 파이프라인에서 떨궈지는 이슈 회피") 의 확장. 후속 axelabs SSOT 의 모든 `@import url()` 도 동일 strip 적용 (sync 스크립트의 정적 규칙).

**Blueprint 전용 패치 2 — Legacy var alias bridge**:

35+ tsx 가 기존 토큰 (`var(--navy)` `var(--white)` `var(--gray-ax)` `var(--gray-light-ax)` `var(--dark-border)` `var(--accent-glow)` `var(--green)` `var(--red)` `var(--navy-light)` `var(--navy-mid)`) 직접 사용. 비파괴 채택을 위해 `src/app/globals.css` 의 `@layer base :root { }` 에서 alias:

```css
--navy: var(--bg-base);            /* dark: #1a0610 (legacy 동일) */
--navy-light: var(--gray-2);
--navy-mid: var(--gray-3);
--white: var(--text-primary);      /* dark: #f2e8ee (≈ legacy #f0f2f5) */
--gray: var(--text-muted);
--gray-light: var(--text-tertiary);
--dark-border: var(--border-subtle);
--accent-glow: rgb(227 255 102 / 0.15);
--green: var(--success);
--red: var(--danger);
```

`--accent` 는 legacy 값 (`#E3FF66`) 을 제거 — colors.css 가 모드별로 자동 주입 (`var(--brand-claret)` light / `var(--brand-neon)` dark). dark 모드에서 `--accent = #e3ff66` 는 legacy hex (`#E3FF66`) 와 byte-equivalent. 즉 `data-theme="dark"` 기본인 한 모든 컴포넌트 무수정 작동, light 토글 시 일관성 있게 claret 톤으로 전환.

**검증 (deployed @ 2026-05-22 PR 머지 시)**:
- `curl https://blueprint.axellc.com/_next/static/chunks/0bd4439~8glcs.css | grep -oE '\[data-theme="dark"\],\.dark'` — lightningcss optimizer 가 selector list 보존
- `--gray-1: #1a0610` (dark block), `--brand-neon: #e3ff66`, `--navy: var(--bg-base)` 확인
- Pretendard CDN 200, `/api/health` localhost + edge 200
- `next build` 컴파일 ~8s, `@import rules must precede` 경고 0

**후속 (별도 PR)**:
- 8+ tsx 의 inline `fontFamily: "'Space Grotesk', ..."` → `var(--font-sans)` 토큰화 (SVG 텍스트). 완료되면 Space Grotesk `<link>` 제거 가능.
- 35+ tsx 의 `var(--navy)/var(--white)/var(--gray-ax)` 직접 사용 → 새 토큰명 (`var(--bg-base)/var(--text-primary)/var(--text-muted)`) 점진 마이그레이션. alias bridge 는 그 동안 보존.
- Light mode 토글 UI (인프라는 준비 — `data-theme` attribute, persistence 만 별도).

**관련 진단**: `/Users/axe/.claude/projects/-Users-axe-blueprint/memory/architecture_blueprint_axe_ui_adoption.md`

### D-matrix-1 — Matrix: Rust + native MCP (no Python SDK)

AXE Labs 첫 Rust 서비스. MCP Streamable HTTP 를 JSON-RPC 2.0 + axum 으로 직접 구현 (Python FastMCP 미사용). 모니터링 서비스 특성상 저메모리 (~15 MB), 단일 바이너리, tokio 기반 concurrent health check 가 핵심 이점.

| 항목 | 결정 |
|---|---|
| 언어 | Rust 1.95 — 기존 Python 3.12+ (frame/hive/blueprint) 과 별 트랙 |
| MCP 구현 | JSON-RPC 2.0 + SSE over axum (native). MCP 프로토콜 레이어가 충분히 단순 (JSON-RPC + SSE) 하여 직접 구현 |
| DB | PostgreSQL 16 (sqlx) — 기존 서비스와 동일 DB 엔진 |
| Docker API | bollard crate — 컨테이너 상태 실시간 수집 |
| 배포 패턴 | blue/green (frame/hive 와 동일 — port 3910/3911 + proxy 3912) |

**기존 결정과의 관계**: D-bp-mcp-1 (Python + FastMCP 기반 MCP 서버 표준) 은 기존 Python 서비스에 그대로 유효. 본 결정은 infrastructure tooling 의 새 precedent — Rust 의 장점 (메모리, 바이너리 크기, 동시성) 이 Python SDK lock-in 보다 우선하는 경우에 한정.

**Rust 마이그레이션 적합성 평가** (2026-05-21): 기존 8개 프로젝트 (frame, blueprint, magnet, stream, artemis, cortex, mysrt, tether) 의 Rust 전환은 모두 No 판정 (mcp Python SDK + anthropic SDK + 한글 PDF 파싱 lock-in). 새 컴포넌트에서 시작하는 것이 미래 후보 — matrix 가 그 첫 사례. **2026-05-26**: cortex 가 두번째 Rust 서비스 — 기존 file-based recall 폐기 후 artifact-first 재출발 (D-cortex 참조).

### D-matrix-2 — Matrix: WAN/인터넷 가용성 모니터링 + ISP 귀책 판별 (2026-06-03)

운영자의 댁내 Wi-Fi/인터넷 장애를 통신사에 민원 제출하려 macmini 시스템 로그 (`/var/log/wifi.log` 11일치 + 통합로그) 를 분석한 데서 출발. 단말 로그는 "이 기기가 해당 시각 인터넷 도달 실패" 는 증명하나 **ISP vs 댁내 (공유기/Wi-Fi) 귀책을 단독 확정 못 함** (6/3 장애는 DNS 실패 + IPv4 경로 99회 상실 = upstream 정합이나 동시에 Wi-Fi 펌웨어 트랩 1회 → 100% 단정 불가). matrix 가 상시 가동 인프라 모니터링 MCP 이므로 "인터넷 회선 가용성" 을 종합 모니터링 항목으로 흡수 — 향후 장애를 분 단위로 자동 기록 + 귀책 자동 판별.

| 항목 | 결정 |
|---|---|
| 구현 위치 | `collector.rs::check_wan()` — `run_check` 의 5 번째 점검. collector 가 제네릭 (`ServiceCheck{name,type,status,detail}`) 이라 **프로브 1 개 추가 = 저장·alert·history·uptime·status board 자동 적용, 스키마 변경 0** |
| 3 프로브 | `wan-gateway` (공유기 ICMP — LAN 링크) · `wan-internet` (공인 anycast ICMP, RTT + 손실%) · `wan-dns` (호스트네임 해석 시간, `tokio::net::lookup_host`). `tokio::join!` 동시 실행 |
| 귀책 판별 | gateway↑ + internet↓ ⇒ **ISP/WAN fault** (warning 발생) · gateway↓ + internet↓ ⇒ 댁내 링크/Wi-Fi. wifi.log 분석이 못 한 구분을 상시 자동화 |
| 컨테이너 ICMP | `cap_add: NET_RAW` + 런타임 이미지 `iputils-ping` (권한상승 없는 unprivileged ping). Docker Desktop NAT 통과 + 공유기 (192.168.55.1) / 외부 (1.1.1.1) 도달 사전검증 (2026-06-03, 손실 0%) |
| 신규 env | `MATRIX_LAN_GATEWAY` (default 192.168.55.1 — 노드별·DHCP 주의) · `MATRIX_WAN_TARGET` (1.1.1.1) · `MATRIX_DNS_HOST` (google.com). 비밀 아님 → vault 등재 불요. `MATRIX_CHECK_INTERVAL` default 60→30 (끊김 해상도) |
| 범위 (v1) | 프로브 + 귀책 alert. 기존 `get_service_history` / `get_uptime_report` 가 `wan-*` 이름에 즉시 작동. **v2 후보** = `get_wan_report` MCP tool (장애 구간 start/end/지속 + 업타임 + 귀책 = ISP 제출용 타임라인). [B-matrix-wan-report-tool](/ops/backlog) |
| 검증 | host `cargo check` 통과 (cargo 1.95). 런타임 검증은 `axe ship` 후 `/matrix/api/status` 에서 `wan-*` 등장 확인 |

**기존 결정 정합**: D-matrix-1 (collector 제네릭 설계) 위에 프로브 추가만으로 성립 — 새 테이블/도구 불요가 핵심. multi-tenant — `wan-*` 는 노드별 자기 회선 모니터라 customer sovereignty 와 자연 정합.

### D-matrix-4 — netheal: 호스트 인터넷 자가치유 데몬 (2026-06-04)

D-matrix-2 가 WAN 끊김을 *감지·귀책* 하면, netheal 은 *치유* — 6/3 운영자가 손으로 한 WiFi off/on 6회 → 재부팅을 자동화. matrix 는 Docker (Linux VM) 안이라 macOS `en1` 을 제어할 수 없어 치유는 호스트로 분리.

| 항목 | 결정 |
|---|---|
| 위치 제약 | 치유는 **호스트 root LaunchDaemon** (`com.axe.netheal`) — matrix=감지/가시화, netheal=치유. Docker 비의존 → 네트워크/컨테이너 동반 장애에도 작동 |
| 치유 사다리 | DNS 플러시 (dscacheutil + mDNSResponder HUP) → DHCP 갱신 (ipconfig set DHCP) → WiFi 바운스 (networksetup setairportpower off/on). 약→강 |
| ISP 인지 백오프 | gateway↑ + 외부↓ = ISP/회선 장애 → WiFi 바운스 futile → DNS/DHCP 1회 후 HOLD. gateway↓ = 로컬 → 풀 사다리 |
| 안전장치 | 디바운스 (~60s 지속돼야 작동) · 쿨다운 90s · 시간당 12회 서킷브레이커 · **자동 재부팅 기본 OFF** (`NETHEAL_ALLOW_REBOOT`, 박스가 frame/hive/matrix 구동) · 전 동작 타임스탬프 로깅 (`/var/log/axe-netheal.log`, 증거 겸용) |
| 구현 | 순수 bash (의존성 0, 감사 용이, `/bin/bash` 3.2 호환). `~/axe-netheal/{axe-netheal.sh, com.axe.netheal.plist, README.md}`. `bash -n` + 스모크 (started + 오발동 0) 검증 |
| 잔여 | 운영자 sudo 설치 (/usr/local/sbin + /Library/LaunchDaemons) + `launchctl bootstrap` → inventory.mdx launchd 표 등재. [B-netheal-install](/ops/backlog) |

**기존 결정 정합**: D-matrix-2 의 gateway-vs-internet 귀책 로직을 그대로 재사용 (감지→치유). multi-tenant — 노드별 자기 회선 치유라 customer sovereignty 와 직교.

### D-cortex — Cortex (AI-native relationship CRM, 2026-05-26)

7 개 결정의 묶음. 강수훈 개인이 Windows + OneDrive 의 단일 `Network_CRM.xlsx` 로 운영해온 1인용 투자 네트워크 CRM 을 platform 안으로 끌어들임 — frame 이 회계 자산을, hive 가 HR 자산을 platform 안으로 끌어들인 것과 동일 패턴. 도구는 개인용이었지만 데이터 (HPE 딜 / AXEV 파이프라인 / LP·SI·FI·Portfolio·Talent 관계) 는 회사가 의존하는 자산이라 한 사람의 OneDrive 에 잠겨있는 게 본질적 위험.

기존 `/Users/axe/cortex` (2026-03 시작한 file-based "VC 심사역 AI 기억 시스템" — 186 markdown topic tree + 16 entity profile + 3,394 keyword DB + 45,903 connection edge) 은 도메인이 유사했지만 모델 (파일 기반 / 단일 사용자 / 자체 router LLM) 이 새 설계와 완전히 다름 → `.legacy.20260526/` 으로 rename 보존 (623 MB, 추후 enrichment migration 참조용).

| ID | 결정 | 핵심 |
|---|---|---|
| **D-cortex-1** | **M6 artifact 모델 Day 1 채택** | Blueprint 의 typed-fact artifact + citation + append-only event log + PARA dispatch 가 Q4 2026 target 에서 Day 1 표준으로 escalation. Cortex 가 platform 의 첫 artifact-first reference impl. 다른 vertical service 도 향후 동일 패턴. frame 의 domain-strict 테이블 (entity/journal/raw_transaction) 은 회계 KSME 제약 때문에 그대로 유지 — `/schemas` endpoint 가 artifact mirror. |
| **D-cortex-2** | **per-user private (owner_id + Postgres RLS)** | 모든 도메인 테이블 (artifact, citation, artifact_event, google_oauth_token) 에 `owner_id` 필드 + `FORCE ROW LEVEL SECURITY` + `cortex_app` NOSUPERUSER 역할 + `SET LOCAL ROLE cortex_app; SET LOCAL cortex.actor = <email>` 패턴. POSTGRES_USER `cortex` 가 docker postgres init 단계에서 SUPERUSER 로 만들어져 RLS 우회 가능하므로 application connection 은 항상 cortex_app 으로 role downgrade. GUC 이름 = `cortex.actor` (`current_user` 가 Postgres 예약어). |
| **D-cortex-3** | **Google Contacts canonical, 단방향 pull default + enrichment 보존 계약** | 흐름: 명함 → Remember → Google Contacts → Cortex (10분 cron). Google 이 base field (이름·전화·이메일·소속·직급) owning. Cortex 는 enrichment (분류 HPE/AXEV/구분1/구분2 / 메모 / interaction / 관계 / deal link / cohort / former_org/title 등) 만 owning. 신규 인물 진입은 항상 Remember/Google 경로. 양방향 sync 안 함. **Sync 보존 계약** (2026-05-29 PM4 hardening — 동일 일자 PM 의 데이터 손실 버그 발견 + 수정): Google → Cortex overlay 는 `GOOGLE_CANONICAL_KEYS` 화이트리스트 (display_name · given_name · family_name · org · title · department · emails · phones · memberships · biographies · userDefined) 만, 그것도 *meaningful* (non-null + non-empty 문자열/배열/객체) 값일 때만. Cortex enrichment 키는 sync 가 절대 안 건드림. **버그 원인**: `upsert_from_person` 의 `UPDATE artifact SET payload=$1` 가 전체 payload 덮어쓰기 → register_person 으로 등록된 person 의 enrichment (display_name 포함) 가 10분 후 sync 실행 시 빈 Google payload 로 destroy 됐음. **증거**: 김승우 (코오롱인더스트리 전무) + 정성화 (한섬비즈온 대표) 신규 등록 직후 audit 가 propose (operator) → 4분 후 edit (system:google-sync, display_name=null + 모든 enrichment 사라짐) 기록. **복구**: append-only audit log 보존 덕분에 propose payload + 후속 operator edit 으로 restore (op='restore' event, actor='system:enrichment-recovery-D-cortex-3'). **동반 fix**: register_person 의 createContact 가 `display_name` 만 보내는데 People API 의 `Name.displayName` 은 server-computed 라 무시됨 → `unstructured_name` 필드 같이 보내도록 `NameCreate` 확장 (mcp.rs + web/views.rs push_one). 향후 신규 등록은 Google 측에 정상 displayName 저장. **검증**: 복구 + 강제 sync → enrichment 보존, sync 는 `memberships=["myContacts"]` (Google meaningful) 만 overlay. **단방향 원칙의 예외 (식별 키 정정 한정)**: PM4c 에 발견 — `unstructured_name fix` 가 ship 되더라도 fix 이전에 생성된 134 Google contact 들의 displayName 은 빈 상태 그대로. 이걸 Google 측에 재push 하는 도구 = `update_contact` MCP tool + `cortex backfill-google-names` CLI ([D-cortex-google-names-backfill](/ops/decisions)). 둘 다 **식별 키 (display_name/given/family, 그리고 phone/email/org/title 등 GOOGLE_CANONICAL_KEYS 의 일부)** 만 push, enrichment 키 (memo/cohort/HPE/AXEV/former_org/former_title) 는 **절대 push 안 함**. updateContact 는 createContact 의 보완재 (역방향 sync 가 아닌 식별 정정), 단방향 pull default 와 양립. **이번에 함정 추가 발견 — Docker layer cache**: 코드 fix push 후 `docker compose build` 만 했을 때 src content 변경에도 캐시된 빌더 레이어가 재사용되어 old binary 가 컨테이너로 들어가는 케이스 발생. `--no-cache` 강제 빌드 + 새 binary 동작 (enrichment 보존) 직접 검증 후 134 backfill 실행. **PM7 재발 — blue/green 동일 binary invariant + 회귀 테스트 추가** (2026-05-29 PM7): 같은 날 저녁 동일 증상 재현. 김승우/정성화 (한섬·스위티) 3명의 memo/cohort/former_org/former_title 이 다시 system:google-sync 의 edit event 로 wipe. **진짜 root cause**: 소스는 이미 PM4 의 overlay-preserving 코드였으나, `docker compose build --no-cache` 가 `cortex-mcp-blue` 한 서비스에만 적용됐고 `cortex-mcp-green` 은 stale 캐시된 옛 이미지 그대로. blue/green 둘 다 `run_loop` 를 동시에 돌기 때문 (leader election 부재) sync 한 번 돌 때마다 race — 새 코드의 보존 INSERT 직후 옛 코드가 같은 초에 destructive UPDATE 로 덮음. **검증법**: `docker exec cortex-mcp-blue sha256sum /usr/local/bin/cortex` 와 `docker exec cortex-mcp-green sha256sum /usr/local/bin/cortex` 가 정확히 같아야 함 (image-level hash 는 빌드 timestamp 비결정성으로 달라도 OK — 안의 binary 만 같으면 안전). **복구**: 운영자 status='paused' → 두 서비스 모두 `--no-cache` 재빌드 + `up -d --force-recreate` → 183 person `restore-from-audit --apply` → sync 재개. **회귀 차단**: `src/google/sync.rs` 에 `#[cfg(test)] mod tests` 5개 unit test 추가 — (1) overlay 가 enrichment 보존 + (2) Google canonical 이 모두 null 일 때 enrichment 보존 (edge case) + (3) wholesale replace 차단 + (4) 멱등 + (5) is_meaningful 가드. `cargo test --bin cortex google::sync::tests` 5/5 통과. 향후 ship hook 에 묶을 것. **운영 invariant 명문화** (CLAUDE.md 의 운영 함정 섹션): sync.rs 같은 critical path 수정 시 blue/green **둘 다** 명시적 `--no-cache` 후 binary sha256 일치 검증 필수. | 2026-05-26 + 2026-05-29 PM4 (sync 보존 + updateContact 확장) + 2026-05-29 PM7 (blue/green race + 회귀 테스트) |
| **D-cortex-4** | **opportunistic push to Google (신규 등록 시에만)** | `register_person` MCP tool 호출 시, 사용자가 Google 에 없는 사람을 입력하면 `people:createContact` 으로 Google 에도 등록 (resource_name 받아 artifact citation 으로 사용). Update push X — Cortex enrichment 필드 (분류·메모·관계·interaction) 는 Google 으로 안 흘러감. Cortex-only person 은 존재하지 않음 (모든 artifact 가 google_contact citation 보유 가능). |
| **D-cortex-5** | **Day 1 multi-user (per-user Google OAuth Web client)** | 각 사용자가 자기 개인 Google 계정 (Remember 흐름의 upstream) 을 Cortex 에 authorize. refresh_token 은 Postgres `google_oauth_token` 에 `pgp_sym_encrypt(token, CORTEX_PII_PASSPHRASE_AXEC)` 로 저장. per-user sync 가 격리 — 한 사용자 sync 실패가 다른 사용자 안 막음. Initial 사용자는 강수훈 (axellc.com) 이지만 architecture 가 multi-user 지원. State JWT (HS256 with CORTEX_JWT_SECRET) 가 OAuth callback 인증 (DB hit 없이 stateless owner_id 추출). |
| **D-cortex-6** | **MCP-only 1차 (Next.js 프론트 없음)** | claude.ai connector + Claude Code 가 primary client surface. **12 tools** (whoami / list_persons / search_person / get_person / register_person / log_interaction / list_interactions / connect_google / disconnect_google / sync_google_now / classify_person / update_person) + RFC 9728 OAuth metadata (server-level + **resource-level** `/cortex/mcp/.well-known/oauth-protected-resource` 둘 다 — D-ops-23 frame 학습) + Google OAuth callback HTML. UI 가 필요해지면 Blueprint 안에 Cortex 페이지 추가 (M6 통합 이후). 별도 Next.js 안 짓기. **→ D-cortex-6.1 로 부분 번복 (2026-05-29).** |
| **D-cortex-6.1** | **read-only 웹 UI 는 cortex binary 안에서 (axum + maud)** | 운영자 "axe.axelabs.ai/cortex 웹에서 볼 수 있게" 요청 (2026-05-29). D-cortex-6 의 "UI 는 Blueprint" 부분만 번복 — Blueprint 분리 X 이유: (a) D-cortex-7 의 단일 binary 운영 ergonomics 유지, (b) 같은 PgPool / RLS / `set_current_user` 헬퍼 재사용, (c) Microsoft federation 코드 (oauth_as.rs 의 server-to-server token exchange + JWKS verify) 가 그대로 cookie 세션용으로 재사용 가능, (d) Blueprint 가 M6 mirror 까지 cortex 데이터를 모름 → cross-service HTTP 호출 회피. **구성**: `src/web/{session,auth,views,mod}.rs`. 세션 = HS256 JWT cookie `cortex_session` (`tok="web"` discriminator, 8h sliding refresh, HttpOnly+Secure+SameSite=Lax, Path=/cortex). 로그인 = Entra ID federation (`/cortex/login/start` → Microsoft → `/cortex/login/callback`, 별도 redirect URI 등록 — Entra app 의 web.redirectUris 에 `https://axe.axelabs.ai/cortex/login/callback` 추가). 페이지: `/cortex/home` 대시보드 (kind 별 count + Google 연결 상태 + 최근 audit event) · `/cortex/{people,orgs,deals,interactions,relationships}` 리스트 (visibility 필터 + 페이지네이션) · `/cortex/a/:id` detail (payload pretty + citations + 최근 20 events) · `/cortex/google/reconnect` (기존 `build_authorize_url` 재호출 wrapper). **read-only**: write 는 여전히 MCP. 향후 write 추가 시 CSRF 토큰 필요 (현재 GET 만이라 미적용). **세션 격리**: MCP `auth::middleware` (Bearer) 와 web `require_session` (Cookie) 은 별도 axum layer 로 적용 — `/cortex/mcp` 가 cookie 우연히 받아도 Bearer 헤더 없으면 401, web 라우터가 Bearer 받아도 cookie 검증 분리. `tok="web"` 미스매치 시 둘 다 거절. | 2026-05-29 |
| **D-cortex-google-push-batch** | **xlsx 적재 후 Google push 큐 운영 — batch + skip + visibility 격리** | xlsx `--create-missing` 이후 Cortex-only person 215~ 명을 Google 에 동기화하려면 행 단위 클릭 비현실적. D-cortex-4 (opportunistic) 안에서 batch 정의 = **사용자 명시 클릭 (체크박스 multi-select + "Push selected (N)" 버튼) 으로 N 건 enqueue, worker 가 1 req/sec 페이스로 Google quota (90/min/user) 안에서 순차 처리**. **데이터모델 — JobRegistry** (`src/web/google_push.rs`): `Arc<Mutex<{jobs: HashMap<JobId,JobStatus>, owner_running: HashMap<owner,JobId>}>>` (in-memory, 프로세스 재시작 시 손실 허용 — `push_one` 의 google_contact citation 중복 거절로 재시도 idempotent). **owner 동시 1 job 락** — 2nd start 가 같은 owner 면 진행 중 job 의 status page 로 `?already_running=1` 와 함께 redirect (에러 페이지 X, 운영자가 진행 중인 batch 가 어디 있는지 즉시 봄). **진행률**: status page = progress bar + 결과 table + 클라이언트 `setInterval(1500ms)` 로 JSON polling — 워커가 끝나면 stop. **owner 격리**: status snapshot 핸들러는 `owner == auth.email` 강제 (다른 사용자 batch 못 봄). **데이터모델 — skip 마킹**: 연락처미상 placeholder 영구 제외 = `kind='attribute', payload={subject_artifact_id, key='do_not_sync_google', value='true'}` 별도 artifact (`classify_person` 패턴 일관, person payload mutation 회피, append-only audit 자동). `list_google_pending` SQL 이 NOT EXISTS 두 종 (citation google_contact + skip attribute) 강제. **D-cortex-9 안전 fix 동반**: 같은 SQL 에 `AND a.visibility='shared'` 추가 — private person 절대 push 후보 안 됨 (배포 전 누락이었음 → 같이 패치). **MAX_BATCH_SIZE = 500** sanity cap. **라우트**: `POST /cortex/google/push/start` (form ids[]) · `GET /cortex/google/push/status/<job_id>` HTML · `GET /cortex/google/push/json/<job_id>` JSON · `POST /cortex/a/<id>/skip-google-push` 마킹 신규/idempotent. **검증** (fake UUIDs 2 개 + concurrency + completion + 락 재획득 모두 OK, Google API 호출 0 회 — NotFound 가 token refresh 이전 단락). skip 후 pending 215→214, DB 에 attribute 1 row 확인. | 2026-05-29 |
| **D-cortex-design-axe-ui** | **cortex 웹 UI 도 axelabs design tokens 그대로 채택 — docs.axelabs.ai 와 같은 시각언어** | D-cortex-6.1 의 inline CSS (`STYLE` const ~200 line) 폐기 → axelabs SSOT (`/Users/axe/axelabs/src/lib/{tokens,styles}/`) 를 가져다 씀. **Sync 메커니즘** — `scripts/sync-axe-ui.sh` (docs.axelabs.ai 의 `scripts/sync-axe-ui.mjs` 패턴 미러). 호스트 빌드 시 axelabs 소스를 읽어 `static/axe-ui/bundle.css` (83 KB) 와 `static/axe-ui/fonts/SarasaFixedK-Regular.woff2` (533 KB) 갱신. **Bundle 구성** = tokens 4 종 (colors / spacing / typography / fonts) + styles 6 종 (reset / components base / form / data-display / dashboard / feedback). components.css 의 `@import` 중 cortex 가 안 쓰는 그룹 (overlays, agent, menus, mobile, layout, indicators, chat-ext, data-views, composites) 은 sync 시 strip. fonts.css 의 `/fonts/SarasaFixedK-Regular.woff2` 절대경로는 cortex 가 자기 static 으로 서빙하도록 `/cortex/static/fonts/...` 로 rewrite. **Binary stamp** — `include_str!`/`include_bytes!` 로 cortex binary 안에 박아 Docker stage 가 axelabs 디렉토리를 못 봐도 빌드 가능 (Dockerfile 도 `COPY static ./static` 한 줄 추가). 라우트 = `GET /cortex/static/axe-ui.css` + `GET /cortex/static/fonts/SarasaFixedK-Regular.woff2` (public, 세션 불요, Cache-Control 1일/30일). **HTML 적용** — `<html data-theme="light">` 로 라이트 모드 anchor + `<link rel="stylesheet" href="/cortex/static/axe-ui.css">`. 컴포넌트 클래스 매핑: `.topbar` → `.axe-topnav`, `.brand` → `.axe-logo`, `.card` → `.axe-card`, `.btn` → `.axe-btn .axe-btn--{primary,secondary,ghost}`, `.metric` → `.axe-metric-card`, `<table>` → `.axe-data-table > __scroll > __table > __th/__td`, `.empty` → `.axe-empty-state`, callout box → `.axe-callout--{info,danger}`, checkbox → `.axe-checkbox`. **Cortex-only 개념** (visibility tag, payload pre, kv list, filter bar, simple pager, flash, login wrap) 은 `.cortex-` prefix 로 분리 inline. **검증**: axe-ui.css 200 (85100 bytes, text/css) · font 200 (533932 bytes, font/woff2) · /cortex/home 200 + data-theme="light" + link stylesheet + .axe-topnav/.axe-metric-card/.cortex-metric-grid 모두 렌더 · /cortex/google/pending 200 + .axe-data-table/.axe-checkbox/.axe-btn--primary 렌더 · 엣지 https://axe.axelabs.ai/cortex/static/axe-ui.css 200. | 2026-05-29 |
| **D-cortex-backfill-orgs** | **person.payload.org inline string → organization artifact + employed_by relationship 일괄 승급 (CLI 2단계)** | 운영자 관찰 (2026-05-29): /cortex/orgs 페이지가 비어있음. person 3,597 개 모두에 회사가 payload.org 안에 inline string 으로만 있고 distinct 약 1,536 개. **자동 normalization 의 함정** — "노루" vs "노루홀딩스" vs "(주)노루홀딩스" 같은 변형을 LLM/trigram 만으로 합치면 잘못 합치는 사고. 따라서 **2단계 워크플로**: Phase 1 = `cortex backfill-orgs --owner <email>` (`src/backfill.rs`) → pg_trgm 활성화 + distinct org 추출 + similarity ≥ 0.6 클러스터 제안 YAML stdout (canonical_name + aliases + 빈도). 운영자 파일 저장 → 편집 (잘못 클러스터된 항목 분리, 합칠 항목 합치고, placeholder/미상은 `skip: true` 추가). Phase 2 = `--mapping <yaml>` 로 dry-run 통계, Phase 3 = `--apply` 실제 INSERT. **Idempotent** — (owner_id, payload->>'name') 으로 org 중복 안 만듦, (edge_kind='employed_by', from_artifact_id, to_artifact_id) 로 relationship 중복 안 만듦, 재실행 무해. **artifact 형식** — `kind='organization', payload={name, aliases:[...], backfill_source:'backfill_orgs_v1'}, visibility='shared'`; `kind='relationship', payload={from_artifact_id, to_artifact_id, edge_kind:'employed_by', source_org_string:<원본>, backfill_source:'backfill_orgs_v1'}, visibility='shared'`. **D-cortex-6.1 read-only 원칙 유지** — 등록 mutation 은 MCP-only 이고 backfill 은 CLI 일회성 도구 (운영자 명시적 의도 분명, 웹 form 추가 안 함). 회사명·employed_by 사실은 민감 아님 → shared 고정. **검증**: 1,536 distinct strings · 1,446 suggested clusters · YAML 5,000+ 줄 stdout 정상 · 소규모 2-org mapping 으로 Phase 2 dry-run + Phase 3 apply + 재apply (existing=2 idempotent) 모두 통과 · /cortex/orgs 페이지 가 빈 axe-empty-state 에서 데이터 있는 axe-data-table 로 전환됨 확인. | 2026-05-29 |
| **D-cortex-google-names-backfill** | **`cortex backfill-google-names` 일회성 CLI — Google contact 측 빈 displayName 정정 (134건)** | unstructured_name fix ship (commit 1326123, cutoff 2026-05-29T06:48:07Z) 이전에 register_person → createContact 로 Google 에 push 된 contact 들의 Google-side displayName 이 NULL 상태. People API 의 Name.displayName 은 server-computed — 옛 코드가 보낸 클라이언트 displayName 무시되고 given/family 도 null 이라 빈 displayName 으로 저장됨. **Candidate filter**: kind=google_contact, ref.source=register_person, artifact.visibility=shared, last_pushed_at 이 cutoff 이전 (또는 NULL + citation.created_at 이 cutoff 이전), citation.ref 의 unstructured_name_backfill 이 v1 아님 (idempotency gate). **per-person flow**: Cortex payload 의 display_name/given_name/family_name 추출 → People API updateContact PATCH (unstructuredName 동봉) → 응답 etag 추출 → RLS tx 안에서 citation.ref 갱신 (last_pushed_at + unstructured_name_backfill v1 마킹 + etag + backfill_source) + INSERT artifact_event 'dispatch' (actor=system:backfill-google-names). **Rate limit**: 1 req/sec (Google quota 90/min/user 안전 여유). **Error 분기**: 4xx etag mismatch 면 fresh getContact 후 1회 retry · 5xx 면 exponential backoff 1s/3s/9s 3회 · 429 면 60s 대기 후 1회 · 그 외 4xx 면 log + skip. **D-cortex-3 정합**: 식별 키만 push, enrichment 키는 절대 push 안 함. **D-cortex-4**: 운영자 명시 --apply 실행만. **D-cortex-9**: visibility=shared 만 candidate, private 자동 제외. **검증**: dry-run candidate=134 (Bucket A=2 김승우/정성화, Bucket B=132). canary 5/5 → full 129/129 success (총 134, 약 2.5분, 1 req/sec). 사후 candidate=0, dispatch event 134개, 김승우/정성화 citation 마킹 v1. 전제 — restore-from-audit 가 Cortex 측 display_name 을 먼저 채워둬야 함. | 2026-05-29 |
| **D-cortex-restore-from-audit** | **append-only audit 기반 enrichment 복원 CLI — `cortex restore-from-audit --owner &lt;email&gt; [--artifact-id &lt;uuid&gt; \| --scan] [--apply]`** | D-cortex-3 sync overwrite 같은 destructive event 후 audit log 기반 enrichment 복원. **알고리즘 — per-key 최신 meaningful 값 picking**: 단일 operator payload_after 만 보면 (예: 김승우 14:41 operator edit 가 이미 깨진 state 위에 former_org 만 추가 + display_name=null) 복원 실패. 키별로 ASC 훑어 마지막 meaningful (non-null + non-empty) 값을 채택. **소스 필터**: `op IN ('propose','edit')` (dispatch/archive/restore op 는 enrichment intent 아님) + actor NOT IN system actors. xlsx-migration 은 enrichment source 로 인정 (operator 가 다른 채널로 못 만진 경우 backup). **대상 키**: Cortex enrichment 키 (비-`GOOGLE_CANONICAL_KEYS`) 만 복원 + display_name 은 canonical 이지만 sync bug 손실 패턴 흔해서 예외 처리. **idempotent**: 이미 복원된 (current payload 가 meaningful) 케이스는 skip 보고. **검증**: D-cortex-3 hotfix 직후 일회성 Python script (177 복구) + 동일 알고리즘 CLI v2 가 14건 추가 발견·복구 (single-payload 정책 미스). 미래 비슷한 사고에 재사용. | 2026-05-29 |
| **D-cortex-mcp-write-tools** | **MCP write tool 표면 완성 — register_organization/deal/relationship + update_contact + archive_artifact + list_organizations/get_organization/list_deals/get_deal/list_relationships (10종 추가, 총 22 tool, register_deal 재설계 — name + contact_person_ids 자동 deal_contact relationship + visibility default private + source_attribute_id, archive_artifact 에 reason/restore alias 추가)** | 운영자 관찰 (2026-05-29 PM3): claude.ai 가 organization/deal/relationship 을 일상 등록할 수 있는 채널이 부재. 그리고 PM4 에 발견된 D-cortex-3 destructive overwrite 버그 fix 과정에서 Cortex → Google 으로 식별 필드 push 도구 (update_contact) 필요성 드러남. 현재 organization 채움 경로는 `cortex backfill-orgs` CLI 하나뿐. 일상 운영 (예: claude.ai 안에서 "이번 미팅에서 A 회사 알게 됐다 → register_organization") 흐름이 시작되도록 register_person 패턴을 4 kind 으로 확장. **register_organization** = (owner, payload->>'name') idempotent (같은 이름이면 existing id 반환, payload 갱신 X), visibility default 'shared', payload 가 additionalProperties: true. **register_deal** = NOT idempotent (같은 title 의 deal 가 round 별로 나옴), target_org_id 가 있으면 owner-scope + kind='organization' 검증, payload 자유. **register_relationship** = (owner, edge_kind, from_artifact_id, to_artifact_id) idempotent, from/to 양쪽 owner-scope 존재 검증, self-loop 차단 (from==to → InvalidArg). **archive_artifact** = `payload.archived=true` + `payload.archived_at=ts` UPDATE + append-only event `op='archive'`. **혁신** — 별도 archive_person/archive_organization/... 안 만들고 한 일반 도구로 통일. `archive=false` 로 restore (역방향, op='restore'). 멱등 — 같은 상태로 호출하면 no_op. visibility tier 와 직교 (안 건드림). **DELETE 차단 우회의 핵심**: artifact_event append-only trigger 가 DELETE/UPDATE 차단 → artifact 자체는 DELETE 가능하지만 CASCADE 로 events 같이 손실. archive flag 로 lifecycle 표현하면 audit 보존 + list/count/push 자동 제외. **List 쿼리 일괄 갱신**: web (fetch_kind_counts/recent_events/artifacts_by_kind/pending_push), MCP (list_persons/search_person via push_person_filters), backfill (distinct_orgs/person_rows) 모두 `NOT COALESCE((payload->>'archived')::boolean, false)` default 필터. get_person(id) 등 ID 직접 접근은 archived 도 반환 (복원 흐름 + audit 보존). **검증** (11/11 통과): (1) register_organization 신규 (2) 재호출 idempotent 같은 id+existing (3) register_relationship 신규 (4) 재호출 idempotent (5) register_deal 신규 (6) 동일 title 재호출 새 id (7) target_org_id 가 person uuid → kind mismatch error (8) archive 4개 (org/2 deal/rel) (9) 재 archive no_op (10) archive=false restore (11) 이전 세션 SMOKE_TEST_ORG_DELETE_ME_BACKFILL 정리. UI 검증: /cortex/orgs 가 archived 항목 안 보임, home dashboard 카운트 archived 제외 정합. | 2026-05-29 |
| **D-cortex-7** | **Rust + axum + sqlx (D-matrix-1 후속 precedent)** | 기존 8개 프로젝트는 Python/TypeScript 인데 Cortex 만 Rust 인 이유: (a) artifact 의 typed discriminated union 과 Rust 의 algebraic type 자연스러움, (b) sqlx 의 compile-time SQL check 가 schema/RLS query 안전성 보강, (c) MCP HTTP 서버 = long-running 단일 process 이므로 Rust 의 panic-free + 낮은 메모리 ergonomics 적합, (d) 단일 binary 배포. Day 1 부터 Rust. matrix (D-matrix-1) 가 첫 Rust precedent, cortex 가 두번째. |
| **D-cortex-8** | **claude.ai connector OAuth = Microsoft 직접 federation (self-hosted AS proxy 아님)** | RFC 9728 의 `authorization_servers` 를 **Microsoft** (`login.microsoftonline.com/<tenant>/v2.0`) 로 둠 → claude.ai 가 Microsoft 의 OIDC metadata fetch → Microsoft `/authorize`·`/token` 사용. cortex 는 RS256 access_token (aud = Application ID URI `https://axe.axelabs.ai/cortex/mcp`) 만 검증 (auth.rs RS256 path). frame 의 "직접-Microsoft 현재 live" path 와 동일. claude.ai connector 의 Advanced field 에 client_id (`60d04ea8-...`) + client_secret 입력 필수 (confidential client). **탐색 후 폐기**: cortex 자체 AS proxy (`/cortex/oauth/{authorize,callback,token,register}` + RFC 8414 metadata + `oauth_authorization_codes` 테이블 + auth.rs HS256 path, src/oauth_as.rs ~620 line) 를 구현했으나 불필요로 판명 → frame oauth.py 와 동일하게 **dormant 보존** (claude.ai client 개선 시 재활성 가능). 사유: `authorization_servers` 를 cortex (path 있는 issuer `https://axe.axelabs.ai/cortex`) 로 두면 RFC 8414 §3.1 의 path-insertion (metadata 위치 = `https://axe.axelabs.ai/.well-known/oauth-authorization-server/cortex`) 때문에 claude.ai 가 cortex sub-path 의 RFC 8414 metadata 를 못 찾음 → origin 의 `/authorize` fallback → blueprint 404. |
| **D-cortex-person-tier** | **person 을 tier (pool\|network) 로 2분 — 자동 sync 연락처(pool) vs 운영자 인맥(network), auto-promote 자동화** | 운영자 관찰 (2026-05-29 PM9): Google Contacts 3,601 person 중 대부분이 일회성/업무 raw 연락처라 "내 인맥" 뷰를 오염. **개념**: pool = Google sync 가 자동으로 가져온 raw 연락처 (의미 부여 X, 손실 없이 보존), network = 운영자가 의도적으로 추가/관리하는 인맥. backfill (tier_backfill_v1): network 625 / pool 2,976. **기본값**: (a) Google sync 신규 person = pool (sync.rs INSERT 시 자동; 기존 person 의 tier 는 enrichment 키 → overlay 가 GOOGLE_CANONICAL_KEYS 화이트리스트 밖이라 절대 안 건드림). (b) register_person (운영자 명시) = network default (claude.ai 에서 의도적 등록 = 이미 인맥); tier='pool' 명시 허용 (행사 명함 일괄 후 추후 분류). (c) import-xlsx (Network_CRM.xlsx = 운영자 큐레이션 인맥) = network — create-missing 신규는 생성 시 tier='network', matched (Google-synced pool) person 도 xlsx 등장 = 인맥이라 system:xlsx-migration 이 network 로 승급 (멱등). **auto-promote (pool→network, 멱등)**: classify_person(attribute 부착) / register_relationship(person endpoint, edge_kind 무관) / log_interaction(participant) 중 어느 액션이든 발생하면 대상 person 자동 승급 — payload.tier='network' + tier_source='auto_promoted_from_&lt;action&gt;' + tier_promoted_at, audit op='edit' actor='system:auto-promote'. 한 helper (auto_promote_to_network) 가 3 handler 의 tx 안에서 호출. **network→pool 강등 없음** — 한 번 인맥은 영원히 인맥. 유일한 역방향 = 운영자 명시 demote_to_pool(person_id, reason) MCP tool (archive_artifact 패턴 미러, payload.tier='pool' + tier_demoted_at + tier_demote_reason, audit actor=operator, 멱등). **노출**: list_persons MCP tool 기본 tier='network' (claude.ai 1차 뷰 = 인맥), web /cortex/people 기본 tier='network' + ?tier=pool\|all 확장 필터 바, home 대시보드 People 카드 = 내 인맥(network) 값 + 풀/전체 sublabel. **D-cortex-3 정합**: tier 는 enrichment 키 → sync 가 절대 안 건드림. 회귀 차단: sync.rs 의 overlay_never_touches_tier_enrichment_key unit test (6 tests). **MCP tool 수**: 22 → 23 (demote_to_pool). | 2026-05-29 PM9 |
| **D-cortex-org-stats** | **home 대시보드 전사(cross-user) 집계 — D-cortex-2 per-user 격리를 처음 의도적으로 여는 표면 (익명 합계 한정)** | 운영자 요청 (2026-05-29 PM10): "대시보드에 조직 전체 통계도". 범위 확정 — **전사 집계(cross-user)** + **전 직원 visible** + **전사 합계만 (직원 익명, per-employee breakdown 없음)**. **구현**: `fetch_org_wide_counts(state)` 가 `set_current_user` 를 호출하지 않아 cortex superuser 연결(rolbypassrls)로 RLS(owner_only) 를 우회 → 모든 owner 의 artifact 를 kind 별 COUNT + distinct owner(사용자) 수만 집계 (sync.rs 의 cross-row 패턴과 동일; per-user 경로는 여전히 SET LOCAL ROLE cortex_app + cortex.actor 로 격리). **격리 보존 핵심**: 개별 레코드도 owner 별 분해도 반환 안 함 — 오직 익명 총합 숫자만 RLS 섬 밖으로 나감 (D-cortex-2 의 "타인 레코드 비가시" 유지, 집계 숫자만 공유). archived 제외. home 에 "조직 전체 (전사 집계 · 사용자 N명)" 섹션 + 익명 안내 callout + `org_metric` (non-link — 전사 합계는 per-user 리스트로 안 매핑되므로 클릭 misleading 방지). **검증**: 집계 person 3600 / relationship 3071 / org 1500 / deal 17 / interaction 1 / users 1. (smoke 잔여 archive 후 정확.) **확장 시 주의**: 직원별 breakdown(attribution, created_by/owner_id 기반)을 추가하려면 개인 인맥 규모가 직원 간 노출 → 별도 운영자 결정 필요. created_by(artifact) + actor(artifact_event append-only) 는 이미 누가 등록·수정했는지 전부 기록 중이라 attribution 데이터는 준비됨. | 2026-05-29 PM10 |
| **D-cortex-customer** | **person/organization 고객 태깅 — 다대다 (1 subject ↔ 여러 case) 수동 태깅** | 운영자 결정 (2026-05-29 PM11): "고객인지 아닌지·어떤 건 고객인지 별도 DB화 — 불편해도 우선 수동, 단 1명이 여러 건(프로젝트 a/b/c)으로 고객 가능". **모델**: 고객 = subject 에 붙은 ≥1개의 `customer_case` attribute (kind='attribute', key='customer_case', value=case 라벨). 한 subject 가 여러 case 로 고객이면 case 마다 별도 attribute → 다대다. 별도 kind/table 안 만들고 attribute 전용 key 재사용 ("별도 DB화" 를 dedicated key 로 충족, 기존 archived/visibility/audit 인프라 그대로). **MCP tool 2종**: tag_customer(subject_id, case, note?, visibility=shared) — (subject, case) 멱등, person 이면 tier network 자동 승급 (고객=인맥); untag_customer(subject_id, case) — 해당 case attribute archive (멱등, tier 강등 X, 다른 case 유지). visibility default shared (전사 고객 카운트 근거). **대시보드**: "고객 N명 / N개 회사" = customer_case 있는 distinct person / organization. MCP tool 수 23 → 25. **확장 여지**: case 를 자유 텍스트 대신 deal/project artifact 로 승급하면 자동 파생 가능 (현재는 수동 우선). **2026-05-29 후속 — customer→account 리네임 + type 차원**: 운영자 "customer 는 일차원적" → 개념을 **account (거래 계정)** 로 리네임 (tag_customer→tag_account, untag_customer→untag_account, key customer_case→account_case, 대시보드 라벨 'Account'). 동시에 **type 차원** 추가 (account_case payload 의 optional `type`, free-form 권장 vocab investor/client/advisor/partner/lp/vendor/portfolio/acquirer/other) — **1 account 가 case 별로 여러 type 가능** (예: investor + client). tag_account 에 type 파라미터 (같은 case 재태깅 시 type 만 UPDATE, 중복 X). 마이그레이션: 기존 58행 account_case 로 리네임 (audit actor='system:account-rename'), 활성 57 AXEC 행 type='investor'. 대시보드 sublabel 에 type별 distinct subject (상위 3). | 2026-05-29 PM11 + 2026-05-29 (account 리네임 + type) |
| **D-cortex-dashboard-redesign** | **home 을 사람 중심 2-row (전사 / 나의) 로 재편 — relationship·kind 카드 제거** | 운영자 피드백 (2026-05-29 PM11): "relationship 은 사람에게 크게 중요한 정보 아닌 것 같다". 기존 home = kind 카드 그리드(people/orgs/deals/interactions/relationships) → 그래프 내부 엣지(relationship) 카운트는 사용자에게 noise. **새 레이아웃 순서**: 구글 연동 → 구글 Push 대기 → 전사 row → 안심 주석 → 나의 row. **각 row 4 지표**: 전체 인원 N명 · 네트워크 N명/N개 회사 · 고객 N명/N개 회사 · 1개월 내 활동 N건/총 N건. 회사 = network person 들이 employed_by 로 연결된 distinct organization. **안심 주석** (전사 row 아래): "네트워크가 아닌 인원은 정보가 조직 내 공유되지 않습니다. 안심하세요." (전사 '총' 은 pool 포함하지만 익명 합계라 개별 pool 인원 정보는 비공유 — 운영자 결정 'pool 포함 총'). **구현**: NetworkStats 한 구조 + NETWORK_STATS_SQL 한 쿼리를 per-user(set_current_user RLS) 와 전사(superuser bypass) 양쪽 재사용. stat_row/stat_cell (non-link — 집계는 per-user 리스트로 안 매핑). 30일 윈도우 created_at 기준 (occurred_at cast 실패 회피). **검증** (나의 row): 총 3600 / 네트워크 624 / 네트워크회사 413 / 고객 0(태그전) / 활동 1·총 1. | 2026-05-29 PM11 |
| **D-cortex-leader-election** | **blue/green run_loop 중 한 쪽만 sync 실행 — `pg_try_advisory_xact_lock` 으로 race 본질 차단 (D-cortex-3 PM7 follow-up)** | D-cortex-3 PM7 가 blue/green binary 불일치 race 였는데 fix 가 "둘 다 --no-cache 재빌드 + binary sha 일치 검증" 운영 invariant 였음. 본질적 root cause = **blue/green 둘 다 run_loop 를 돈다는 것 자체**. binary 동일성은 race 가 일어나도 같은 결정을 내리는 우회. **본질 = race 자체 차단** → leader election. **구현**: `src/google/sync.rs::run_cycle` 시작 시 `state.pool.begin().await` 로 transaction 열고 `pg_try_advisory_xact_lock(SYNC_LEADER_LOCK_KEY)` (i64 const = 2026052907 = PM7 incident 날짜) 시도. 성공 시 leader → `run_cycle_inner` 실행 → `tx.commit()` 시 advisory_xact_lock auto-release. 실패 시 INFO 로그 (`sync cycle: leader lock held by other instance, skipping`) 후 즉시 return. **Transaction-scoped lock 선택 이유**: session lock 은 sqlx pool 의 connection 재사용으로 release 대상 backend 가 모호. xact lock 은 COMMIT/ROLLBACK/disconnect 모두에서 auto-release → leader 인스턴스가 panic·crash·OOM 으로 죽어도 PG backend session 종료 시 lock 즉시 해제, 다음 cycle 에 다른 인스턴스가 leader. 무인 페일오버, lock 영구 stuck 위험 없음. **부수효과**: Google API quota 절반 (이전 2× 호출 → 1×), DB 부하 절반. **Race 차단 일반화**: PM7 같은 binary 불일치 뿐 아니라 코드 수정 race / 동시 sync 가 같은 person 을 두 번 처리하는 모든 race 가 leader 한 명 보장으로 자동 차단. **Ship hook 자동화 (Dockerfile 통합)**: `cargo build --release && cargo test --release --bin cortex google::sync::tests` 가 Dockerfile RUN 한 단계 — test 실패 시 image 생성 자체 불가 → 옛 destructive 코드가 production 으로 흘러갈 수 없음. 매 `docker compose build` 자동 검증. **운영 함정 갱신** (CLAUDE.md): blue/green 동일 binary 가 강제→권장 으로 격하 (leader election 도입 후엔 binary 가 달라도 한 쪽만 실행). 새 함정 = "skip 로그가 한 쪽에서 항상 안 보이면 그 쪽이 leader 가 못 됨 (DB pool 문제)". **검증**: (1) psql 두 세션 동시 시도 → 한 쪽만 `t`, 다른 쪽 `f` 받음 (mutual exclusion). 첫 세션 commit 후 두번째 시도 → `t` (auto-release). (2) Docker rebuild --no-cache both + recreate → 같은 binary sha 확인 + 첫 sync cycle 후 blue/green 중 한 쪽만 "owner sync ok", 다른 쪽 "leader lock held, skipping". (3) `cargo test` 5/5 통과 (회귀 차단). | 2026-05-29 PM8 |
| **D-cortex-9** | **artifact visibility tier (`shared`\|`private`) — owner 의 RLS 섬 밖으로 무엇이 나가는가를 통제하는 propagation gate** | soohun.kang 2026-05-28 "Private Partition" 요청. **핵심 재구성**: 현재 모든 artifact 는 이미 RLS (`owner_only`: `owner_id=current_setting('cortex.actor')`, 앱 role `NOSUPERUSER NOBYPASSRLS`) 로 owner 본인에게만 보임 — *타인·관리자·감사자가 볼 수 있는 경로 자체가 없음*. 따라서 `private` 플래그는 본인 vs 타인 격리가 아니라 **데이터가 owner RLS 섬을 벗어나는 미래 표면을 막는 게이트**. 그 표면 = **Blueprint M6 artifact mirror** ([D-bp-artifact-1](/ops/decisions), [/architecture/artifacts](/architecture/artifacts)): Blueprint `Artifact` 테이블은 `workspace_id`+`entity_id` scope 이고 `owner_id`·`visibility` 컬럼이 없어, mirror 시 owner 격리가 풀림. M6 이 아직 📐 설계 (2026 Q4) 라 **mirror 가 생기기 전 지금이 visibility 를 박을 적기**. **계약**: 모든 outward propagation (Blueprint mirror / org export / dashboard) 은 `WHERE visibility='shared'` 강제, `private` 은 절대 RLS 섬을 안 떠남. owner 본인 세션은 shared+private 모두 봄. **기본값 정책**: `shared` (Google 동기화 base 연락처는 이미 Google 에 존재하는 저민감 canonical) — 단 민감 분류(HPE 등)·xlsx import 의 sensitive 컬럼·명시 flag 는 `private`. 신규 sensitive 데이터(HPE 138건)가 아직 미유입(`B-cortex-xlsx-import-run`)이라 backfill 불필요. **단계**: Phase 0 (지금) = `artifact.visibility` 컬럼 + `list_persons(visibility=, exclude_classification_key/value)` 필터 + write tool `visibility`/`skip_audit_payload` 파라미터 (§6 스톱갭). Phase 1 = Google contactGroup sync 화이트/블랙리스트 (유입 차단). Phase 2 (M6 착수 시) = Blueprint mirror 계약 + field-level privacy (payload `private` 하위객체 + 기존 `CORTEX_PII_PASSPHRASE` pgp 재사용) + private artifact 의 audit redact. Phase 3 (opt-in) = client-key sealed E2E (search·Google push 포기 trade-off). **visibility 결정 UX**: 기본 shared, write tool 4종 description 의 SENSITIVITY RULE 로 커넥터 LLM 이 민감 가능 내용에 한해 저장 전 'private/shared?' 확인 (MCP 는 요청-응답이라 직접 못 물음 → LLM-guidance). **공유 인물 + 비공개 메모** = person `shared` + 별도 `classify_person(visibility=private)` attribute (별개 artifact). `log_interaction` 도 visibility 보유 (private 미팅 → participant relationship 도 상속). | 2026-05-28 |
| **D-cortex-google-drift** | **Google sync 가 인맥(network)의 소속/직급을 다른 meaningful 값으로 덮을 때 drift 알림 기록 — overlay 와 독립한 additive notification 레이어** | Google sync 는 D-cortex-3 에 따라 canonical 필드를 자동 overlay (Google = canonical). 하지만 org/title 이 "한 의미있는 값 → 다른 의미있는 값" 으로 바뀌면 = 이직/승진 신호 → 운영자가 대시보드에서 검토하도록 drift 레코드 생성. **overlay 자체는 안 건드림** — drift 는 순수 additive. **판정 (overlay 와 별개)**: `is_meaningful_change(old,new)` = 공백 정규화 후 둘 다 non-empty 이고 서로 다름. null→value(채움)·value→null(지움)·whitespace-only 차이는 drift 아님. tier='network' 만 (pool = raw Google dump 의 잡음 변화 무시). org·title 두 필드 검사. **레코드**: `kind='attribute'`, key='google_drift', payload `{subject_artifact_id, field:'org'\|'title', old, new, detected_at, source_etag, acknowledged:false}`, visibility='shared', created_by='system:google-sync' + artifact_event op='propose'. **멱등**: 같은 (subject, field, new) 의 활성(archived=false, acknowledged=false) drift 가 있으면 skip. 모두 sync tx 안 (이미 RLS-scoped). **웹**: home 에 "🔔 변화 감지 (Google)" 카드 (미확인 N>0 시) → `/cortex/google/drift` 목록 (이름 link · 변화 "소속/직급: old → new" · 감지일 · [확인]) → `POST /cortex/a/:id/ack-drift` (payload.acknowledged=true + acknowledged_at + op='confirm' event, RLS-scoped). D-cortex-6.1 read-only 예외 (push/skip 처럼 운영자 lifecycle 액션). **백필 안 함**: 기존 audit 의 org/title 변경은 운영자 cleanup 이라 진짜 커리어 변화 아님 — going-forward 만. **회귀 차단**: sync.rs `#[cfg(test)] mod tests` 에 9 drift test 추가 (총 15) — Dockerfile `cargo test ... google::sync::tests` gate 가 강제. | 2026-05-29 |

**부수 효과**:
- `/Users/axe/cortex` 폴더 폐기 (`cortex.legacy.20260526/`), `cortex.axellc.com` 도메인 라우트 삭제 (cloudflared `~/.cloudflared/config.yml` 정리, mysrt.axellc.com 만 보존)
- 32xx 포트 재할당: 3200 postgres / 3210 mcp-blue / 3211 mcp-green / 3212 proxy (frame 3700/3710/3711/3712 패턴)
- `customers.yaml`: `sso.apps.cortex_mcp` (appId=`60d04ea8-5d7b-453e-a4c0-af421b4689f5`, az cli 등록 2026-05-26) + `services.cortex` (5 secrets: DB_PASSWORD, PII_PASSPHRASE_AXEC, JWT_SECRET, AZURE_CORTEX_MCP_CLIENT_SECRET, GOOGLE_OAUTH_CLIENT_SECRET)
- Entra app: frame_mcp / hive_mcp / blueprint_mcp 와 동형 (signInAudience=AzureADMyOrg, requestedAccessTokenVersion=2, identifierUris=`https://axe.axelabs.ai/cortex/mcp`, scope `mcp.access`, webRedirectUris=claude.ai+claude.com `/api/mcp/auth_callback`)
- Postgres 5 테이블 (artifact / citation / artifact_event / mcp_schema / google_oauth_token), GIN+btree+partial index, append-only trigger, 4 RLS policy (owner_only USING+WITH CHECK)
- 10 MCP tools — auth middleware (Entra ID JWKS 1h TTL cache + RS256 + audience-issuer-exp 검증) 통과 후 `Extension<AuthContext>` 로 owner_id 사용
- Google OAuth Web flow + People API client + per-user sync loop (600s 간격, `system:google-sync` actor 로 artifact_event 기록)

**운영자 작업 — 2026-05-28 production live 완료**:
1. ✅ 5 secret vault push (`cortex/axe/{db-password, pii-passphrase-axec, jwt-secret, oauth-client-secret, google-oauth-client-secret}`) — 모두 stdin pipe (jq -Rsc) 로 shell history·context 미경유
2. ✅ Google OAuth Web client — 신규 GCP 프로젝트 `Cortex` (id `cortex-497605`, 옛 `Gemini`/Network Manager 폐기 예정). People API enable + consent screen External/Testing + client (`135512942819-...apps.googleusercontent.com`)
3. ✅ Cloudflare ingress — `axelabs` 터널은 Cloudflare **Dashboard remote-managed** (로컬 config.yml 무시 — `Updated to new configuration version=N` 이 증거). `axe` 의 기존 CF API token (vault `Cloudflare API - axelabs`) 으로 `PUT /accounts/.../cfd_tunnel/.../configurations` 하여 `axe.axelabs.ai` path=`cortex(/.*)?` → `host.docker.internal:3212` 추가
4. ✅ `docker compose up -d --build` (postgres 3200 + mcp-blue 3210 + mcp-green 3211 + proxy 3212). `cortex migrate` (2 migration: artifact schema + oauth_authorization_codes)
5. ✅ claude.ai connector 등록 (URL + client_id + client_secret) → Microsoft OAuth → 12 tools 노출 + whoami 검증
6. ✅ connect_google → 브라우저 consent (kangsoohun@gmail.com, Test users 등록 필요) → "✓ Google 연결 완료"
7. 🔧 (잔여) `cortex import-xlsx` 660 행 enrichment 1회 마이그 + `axe mcp publish` catalog 등재 — [B-cortex-xlsx-import-run](/ops/backlog), [B-cortex-mcp-catalog-publish](/ops/backlog)

**시행착오 (전부 [known-gaps](/ops/known-gaps) 등재)**: DB password 의 base64 `/` 가 URL port 파싱 깨뜨림 (PgConnectOptions builder 로 회피) · docker compose env_file 가 따옴표 literal (axe secret pull quote 가 postgres auth 깨뜨림) · resource-level RFC 9728 path 누락 · RFC 8414 path-insertion · MCP tool property key 한글 (`메모`) 이 tools/list 전체 거부 · Cloudflare remote-managed tunnel · Docker image 가 옛 코드라 cargo build 만으론 부족 (`--build` 필수).

상세: [services/cortex](/services/cortex), [/Users/axe/cortex/docs/operator-setup.md](https://github.com/soohunkang/cortex/blob/main/docs/operator-setup.md), [/Users/axe/cortex/CLAUDE.md](https://github.com/soohunkang/cortex/blob/main/CLAUDE.md).

### D-vault-mcp-catalog — MCP Connectors catalog view (Vaultwarden org collection)

axelabs 자체 운영 MCP (frame, hive, blueprint, ...) 의 OAuth 등록 정보 4 조각 (이름, MCP URL, client_id, client_secret) 을 사용자가 `claude.ai/customize/connectors` 등 MCP host 에 등록할 때마다 4 곳에서 찾는 비효율 해소. **catalog view** = Vaultwarden 의 `MCP Connectors` organization collection (org-wide read access). 사용자는 Bitwarden 브라우저 확장만 열면 URI 매칭으로 자동 suggest.

| 항목 | 결정 |
|---|---|
| **SoT 불변** | `/Users/axe/.axe/customers.yaml customers.<cust>.sso.apps.*_mcp` (client_id, application_id_uri, scopes) + Vaultwarden item `<svc>/<cust>/oauth-client-secret` (또는 `blueprint/<cust>/mcp-client-secret`) for secret. **데이터 중복 0** |
| **catalog 위치** | AXE Vaultwarden org `0c5d8bbd-ad85-42b4-8b8a-2849031981b1` 의 `MCP Connectors` collection ID `1a62e754-6e47-43e0-a99a-cf71c37b8638`. 4명 org 멤버 모두 access (ai@/soohun@ = manage, taehun@/jinwoo@ = read-only) |
| **item shape** | Login item per `*_mcp` app. title = `"{Svc} MCP ({cust})"`, username = client_id, password = client_secret, custom fields = MCP URL / Tenant ID / Scopes / Vault path, URIs = `claude.ai/customize/connectors` + `claude.com/customize/connectors` + MCP endpoint (모두 "Starts with" match → 브라우저 확장 자동 suggest) |
| **publisher** | `axe mcp publish [--customer axe]` — `/Users/axe/.axe/bin/axe` 의 `cmd_mcp_publish`. 매니페스트 순회 + vault fetch + bw create/edit upsert. idempotent. `axe mcp list` 는 read-only 미리보기 (vault 미접근) |
| **hook** | `axe secret rotate <ENV> --service <svc>` 가 MCP `client_secret` 회전 시 `axe mcp publish` 자동 호출 → catalog 의 password 도 즉시 갱신. 사용자가 옛 secret 으로 claude.ai 에 등록하다 OAuth 거부 받는 함정 사전 차단 |
| **신규 MCP 추가 시** | (1) Entra app 등록 + customers.yaml 의 `sso.apps.<svc>_mcp` 4-key (client_id, application_id_uri, scopes, client_secret_env) 추가 + services manifest 의 secret entry 등재 → (2) `axe secret push` → (3) `axe mcp publish`. 3 단계 표준 |
| **realchoice (sovereignty 격리)** | customer 측 vault 에 customer 자체 `MCP Connectors` collection. operator 의 catalog 에 customer secret 비포함. [B-customer-sovereignty-architecture](/ops/backlog) Q3 milestone |

**함정 (도입 직후 발견)**: `customers.yaml customers.axe.sso.apps.frame_mcp.client_secret_env` 가 5/21 hive_mcp 등재 후에도 D-ops-15.5 의 "proxy path only" 주석 형태로 stale 처리되어 있었음 (실제 vault item 존재 + claude.ai 가 confidential client 요구). 본 D 도입 시 정정 — frame_mcp 도 secret 포함하여 catalog publish. 같은 함정 = 미래 새 MCP 추가 시 `client_secret_env` 누락 시 catalog item 이 (public) 으로 잘못 publish 됨. `axe mcp list` 출력의 "vault item" 칸이 `(public)` 이면 점검 신호.

### D-ops-42 — 배포 SSOT 아키텍처 (origin/main SHA = 배포 진실원천)

다중 동시 Claude Code 세션이 repo 당 1 working tree + 1 `main` 을 공유하던 구조가 만든 배포 엉킴을 근본 해소. **원칙 한 줄**: `origin/main 의 commit SHA = 배포의 SSOT`. working tree·로컬 main·실행 중 컨테이너는 전부 그 SHA 의 파생·일회용 투영. AXE 가 docs·backlog(matrix-postgres, [D-matrix-3](/ops/decisions))에 이미 적용 중인 SSOT 규율을 **배포**로 확장한 것. 전체 본문 = [/ops/runbook/deploy-ssot](/ops/runbook/deploy-ssot).

**동기 (실측 incident, 2026-06-04)**: 공유 tree + 공유 `main` 이 세 가지 고장을 동시에 발생:

| 고장 | 메커니즘 |
|---|---|
| push 거부 | 타 세션이 먼저 push → 로컬 `main` 이 origin 과 diverge → non-fast-forward → push blocked |
| WIP 혼재 | 서로 다른 feature 의 uncommitted 변경이 한 tree 에 누적 → 누구도 자기 slice 만 clean commit 불가 |
| dirty 누출 | `axe deploy` 가 dirty working tree 에서 이미지 빌드 → uncommitted 코드 prod 누출 + push 전 배포 가능 |

구체 사례: 이미 배포까지 끝난 blueprint fix (commit `b4067504`) 가 main diverge + 타 세션 WIP 로 수 시간 push 불가 → 운영자가 수동 reconcile (stash→rebase→push). 추가 발견 = 운영자 `axe` CLI (364KB) 자체가 **버전 미관리 + in-place 편집** = 같은 병의 가장 깊은 사례 (CLI 가 배포 도구인데 자기 자신은 SHA 추적 밖).

**컴포넌트 — 각자 제거하는 고장**:

| 컴포넌트 | 동작 | 제거하는 고장 |
|---|---|---|
| **A. 작업 격리** | `axe work <svc> <slug>` = 세션별 git worktree (`~/.worktrees/<svc>/<slug>`) off origin/main. 정규 repo 는 fast-forward 전용 "main mirror" 강등 (손편집 금지) | 공유 tree 경합 · WIP 혼재 · stash 더미 = **발생 불가능** |
| **B. SHA 에서만 빌드** | deploy 가 working tree 가 아니라 pushed SHA 의 clean checkout (`git worktree --detach`/archive) 에서 빌드. 이미지 태그 `<svc>:<sha>`. push 안 된 SHA 배포 거부 | deploy-before-push + dirty-tree 누출 = **구조적 불가능** |
| **C. Deploy lock** | 서비스당 배포 직렬화 (matrix-postgres `pg_advisory_lock` 우선, 파일락 fallback) | 두 세션이 같은 서비스 blue/green 동시 flip 불가 |
| **D. Provenance + drift** | 컨테이너 `org.axe.git_sha` 라벨. `axe health`/`axe host` 가 color 별 실행 SHA + origin/main 대비 drift 표시 | "지금 라이브가 git 과 일치하나" 에 항상 답 (silent drift 소멸) |
| **E. 잘못된 경로 제거** | `axe deploy` 가 tree 입력 폐기. `git push origin main` 직접 = pre-push 훅 + GitHub branch protection 으로 기계적 차단. `axe ship` 만 main 전진 | honor-system 의존 (현재 = 사회적 규약뿐) 제거 |
| **F. 통합 ship** | `axe ship` (worktree 에서): fetch → origin/main rebase → SHA build+test → push (main ff) → 그 SHA deploy (lock 하) → mirror ff → shiplog | 오늘 운영자가 손으로 한 reconcile 전 단계가 한 명령 |

**스테이지 상태기계** (blue/green 이 이미 3스테이지 제공):

```
built → canary(passive color, 트래픽 0) → [migration-validation 게이트] → live(active flip) → previous(직전 active = 즉시 롤백)
```

**migration-validation (안전 척추)**: 스키마 마이그레이션 포함 릴리스는 flip 전에 prod DB 의 ephemeral clone 에 마이그레이션을 적용·검증 (`axe drill`/backup 스냅샷 기계 재사용). 본질적 이유 = blue 와 green 이 **DB 를 공유**하므로 깨지는 스키마 변경은 blue/green 으로 카나리가 *불가능* (green 용 마이그레이션이 blue 도 즉시 오염). 이 게이트가 blue/green 이 못 막는 유일한 위험을 덮음. 대상 = blueprint 대기 migration 2개 (`add_user_entra_oid`, `add_entity_legal_name`), hive alembic.

**rollout (additive → canary → cutover)**: 전부 기존 동작과 공존 (additive) → `--dry-run` + passive-color 카나리 (flip 안 함) 검증 → cutover (기본값 flip + 가드 활성화, escape hatch 포함) 는 게이트됨 → 되돌림 가능. 진행 중 WIP 보존.

**채택 안 함**:

- **별도 staging 환경** (staging.axelabs.ai + 독립 DB/터널) — 단일 Mac mini 호스트에서 새 실패유형은 안 잡으면서 비용·parity 만 증가. canary + migration-validation 이 실위험을 이미 커버.
- **작업추적에 새 "스테이지" 축** — roadmap/backlog/updates 가 이미 작업 스테이지 파이프라인 ([D-matrix-3](/ops/decisions)). 배포 스테이지와 작업 스테이지를 혼동하지 않음.

**정합**: [D-ops-16](/ops/decisions) (docs drift = release-gate `axe ship`) 의 release-gate 를 배포 진실원천까지 확장 · [D-matrix-3](/ops/decisions) (matrix-postgres SSOT) 의 SSOT 규율을 배포로 확장 · [Blue/green deploy](/ops/runbook/deploy) 의 alias swap 메커니즘 재사용.

### D-ops-44 — Vault 비밀 주입 = SSH 환경 raw-bw keychain-free (`axe vault unlock` 금지)

이 머신은 AXE 의 Mac mini 이고 **Claude Code 세션은 SSH(loopback) 위에서** 돈다. 운영자는 Windows 에서 `ssh axe@100.127.210.30` (Tailscale) 로 들어온다. macOS 는 **SSH 세션이 GUI login keychain 에 쓰는 것을 거부**한다 ("User interaction is not allowed"). 그 결과 vault 비밀을 *넣는* 통상 경로 (keychain 세션 의존) 가 SSH 컨텍스트에서 전부 막힌다. 본 결정 = SSH 환경에서 비밀을 vault 에 넣는 **정답 경로를 keychain-free raw-bw 로 고정**하고, 이를 `/Users/axe/CLAUDE.md` 에 강제 규칙으로 박제한 것. 운영자-facing how-to = [/architecture/secrets#ssh-환경에서-vault-비밀-주입-keychain-free-raw-bw](/architecture/secrets).

**금지 (SSH 에서 전부 실패)**:

| 금지 | 이유 (SSH 에서 깨지는 메커니즘) |
|---|---|
| 운영자에게 `axe vault unlock` 시키기 | unlock 의 keychain-cache 단계가 SSH 세션에서 crash |
| `axe secret push/pull/check` 로 비밀 PUT | 내부 `_vault_env` 가 keychain 세션만 읽음 (`security find-generic-password -s axe.vault.session -a ai@axellc.com`) → SSH 에선 "vault session not found" |
| osascript `display dialog` | GUI 다이얼로그 = 원격/Windows 운영자에게 안 보임. 운영자-타이핑 비밀은 `read -rs` 로 받음 |

**정답 = keychain-free raw-bw** (운영자가 자기 인터랙티브 셸 — Windows `ssh axe@100.127.210.30` 또는 Mac 터미널 — 에서 직접 실행). 에이전트는 스크립트를 **ASCII-only · 주석 없음 · `bw unlock` 을 단독 한 줄**로 공급한다 (한 블록으로 붙여넣으면 unlock 프롬프트가 바로 다음 줄을 비밀번호로 삼켜버림). 단계:

1. `export NODE_EXTRA_CA_CERTS=/Users/axe/.axe/vault/certs/rootCA.pem` — Vaultwarden private CA, 필수.
2. **자기 단독 줄에서** `export BW_SESSION="$(bw unlock --raw)"` — master-pw 프롬프트. TTY-flaky 폴백: `read -rs PW; export BW_SESSION="$(BW_PASSWORD="$PW" bw unlock --passwordenv BW_PASSWORD --raw)"; unset PW`.
3. raw `bw create` / `bw edit` 로 Login item 생성·갱신 — **name** = `customers.yaml` 의 `services.<svc>.secrets[].vault` 경로 (예 `gate/axe/jwt-secret`), **username** = env var 이름, **password** = 스크립트 안에서 생성/읽은 값 (`$(openssl rand -hex 32)` / `$(cat key.pem)` / 상수 — echo·history·argv·agent-context 어디에도 안 남김) → `bw sync` → `unset BW_SESSION`.

**검증 (에이전트는 검증 불가)**: BW_SESSION 이 운영자 셸 안에만 살아 있으므로 에이전트는 결과를 직접 확인할 수 없다. 운영자의 `created/updated` + `synced` 출력이 곧 확인이다. `axe secret check` 호출 금지 (keychain 필요).

**keychain 세션이 필요한 곳 = deploy/ship 시점 한정**: `axe ship`/`axe secret pull` 은 그때 keychain 세션을 읽는다. SSH 에서 GUI 없이 그 세션을 채우는 법 = `read -rs KCPW; security unlock-keychain -p "$KCPW" ~/Library/Keychains/login.keychain-db; unset KCPW` (headless unlock) → `axe vault unlock` → `axe ship`. **비밀을 *넣는* 작업은 이 단계가 불요** — raw-bw 가 keychain 없이 vault 에 직접 쓴다.

**정합**: [D-ops-17](/ops/decisions) (vault = canonical SoT, manifest 매핑) · [D-ops-40](/ops/decisions) (osascript hidden-dialog 패턴 — GUI 세션 전제라 SSH 컨텍스트에선 본 raw-bw 가 대체). 더 깊은 잔여 격차 (=`_vault_env` 가 keychain 만 읽고 env-session 폴백이 없는 것) 는 [/ops/known-gaps](/ops/known-gaps) 에 분석 기록.

## ⚠️ 함정 — 회피 필수

| # | 함정 | 결과 | 회피 |
|---|---|---|---|
| 1 | pull-polling 배포 | 1시간+ lag | push (D6) |
| 2 | 자체 mTLS | 운영 부담 | Tailscale + restic 암호화 |
| 3 | schema-per-customer (한 macmini 다중 customer) | 격리 약 | macmini 1대/customer |
| 4 | OAuth 1차 시도 | 도메인 검증 등 함정 layer | Phase 3 deferral 까지 |
| 5 | 2-level subdomain SSL | 유료 ($10/cert/월) | 1-level + path |
| 6 | service-only domain (`frame.axelabs.ai`) | customer 격리 불가 | `{customer}.axelabs.ai/{service}` |
| 7 | `.local` 도메인 | mDNS 충돌 | platform/corporate 분리 |
| 8 | `axellc.com` 으로 platform 트래픽 | corporate ↔ platform 혼탁 | `axelabs.ai` 만 |
| 9 | cloudflared path strip 가정 | 라우터에서 prefix 못 찾음 | 서비스에 prefix mount |
| 10 | cloudflared SIGHUP 직접 호출 | process 죽음 | `docker restart` |
| 11 | frame container 직접 cloudflared 바인딩 | swap 불가 | host-side proxy 경유 |
| 12 | 다운타임 deploy | 사용자 5xx | blue/green |
| 13 | `.claude/settings.json` 의 mcpServers | silent 무시 | `.mcp.json` (D-ops-7) |
| 14 | Microsoft Application ID URI 와 scope prefix 불일치 | AADSTS9010010 | 둘 다 같은 URL prefix |
| 15 | Allow public client flows: No + secret 없음 | AADSTS7000218 | Yes 또는 secret |
| 16 | accessTokenAcceptedVersion=2 + Anthropic 의 aud check | mcp_client_invalid | null/1 (v1 token, aud=URI) |
| 17 | App ID URI 삭제 cleanup | AADSTS500011 | 복원 후 propagation 대기 |
| 18 | compose `environment:` 의 `$VAR` default-empty 치환 (docker-compose `dollar-brace VAR colon dash brace` 형식) 가 `env_file` 값을 shadow | 컨테이너 secret 빈 값 | `environment:` 에서 비밀 var 제거 (D-ops-18) |
| 19 | compose subdir (`REPO/docker/` 형태) 의 Docker auto-load `.env` 부재 | `$VAR` substitution (docker-compose 변수) 실패, 컨테이너 미부팅 | `ln -sf ../.env docker/.env` symlink (D-ops-18) |
| 20 | `axe secret pull` 옛 버전이 .env 전체 덮어씀 | 비밀 외 config 라인 wipe (2026-05-21 blueprint 사고) | 코드 fix 완료 (merge-mode). pull 출력에 "non-managed lines preserved" 확인 |
| 21 | portal-등록 Azure app 에 owner 미지정 | `az ad app credential reset` 권한 거부 | 운영자를 portal 의 Owners 탭에 명시 추가 (D-ops-19) |
| 22 | `az ad app credential reset` (no `--append`) | 기존 secrets 전부 삭제 → overlap window 없음 | 항상 `--append` (D-ops-19) |
| 23 | 새 fact 도입 시 schema 를 AXE 가 직접 정의 시도 | AXE 가 schema authority 부담 → 작업 정체 + drift | **MCP 가 schema 권위** (frame/hive 등 — 자기 도메인 `/schemas` endpoint). 자기 도메인 MCP 없으면 LLM 자율 free-form. Blueprint 는 discovery / mirror only ([D-bp-artifact-1](/ops/decisions)) |
| 24 | artifact extraction 을 Anthropic API direct call 로 구현 | per-token 비용 + per-user attribution 손실 + Max plan 무제한 활용 못 함 | Claude Code OAuth (Max plan) 통해 호출 — Teams bot / 로컬 CLI / 웹 모두 same surface. API direct 는 대량 batch / vision-heavy 만 ([D-bp-artifact-7](/ops/decisions)) |

## 결정 변경 절차

새 결정 또는 기존 결정 변경 시:

1. **multi-tenant-platform-plan.md** 또는 **frame/DECISIONS.md** 에 추가 (해당하는 곳)
2. 본 페이지의 표 갱신
3. 영향 받는 다른 docs 페이지 수정
4. customer admin / 직원에게 통지 (영향 받는 경우)

## 회고 주기

분기마다 (Jan/Apr/Jul/Oct 15) DECISIONS 회고:

- 채택한 결정 중 후회 있는 것
- 함정에 빠진 사례
- 새로 발견된 함정
- D-ops 회수번호 (deprecate 결정)

## 참고

- `/Users/axe/multi-tenant-platform-plan.md` — 마스터 plan + D-config-N
- `/Users/axe/frame/DECISIONS.md` — frame D-ops-1~15
- `/Users/axe/hive/DECISIONS.md` — hive D-hive-1~25
- `/Users/axe/blueprint/docs/project-blueprint.md` — blueprint 마스터 (5 분과)
- `/Users/axe/CLAUDE.md` — 포트 · launchd 인벤토리
