DeepSeek Harness · 시스템 맵

Profile / Bundle / Patch: 사용자가 제품을 어디까지 바꿀 수 있나

소스를 건드리지 않고도 깊은 능력을 바꿀 수 있어요: 설정은 빈 배열 위에 patch 네 층을 쌓고, 늦게 적용한 쪽이 이기고, 같은 항목을 맞히면 통째로 교체해요.

강의 목표읽고 나면 세 가지를 말할 수 있어요: DSH 설정이 왜 Bundle(배포판 기본값), 사용자 patch(Profile 층 + Home 층), --patch(일회성 실험)로 나뉘는지, 층끼리 우선순위는 어떻게 잡히는지; patch가 같은 항목을 맞힐 때 왜 통째 교체인지, 그 선택이 어떤 반직관적 결과를 만드는지; 그리고 dsh --dump-config가 최종 적용 설정을 어떻게 펼쳐 보여 주는지.
인터랙티브 데모 · Patch 겹층 샌드박스

아래 샌드박스는 설정 네 층을 켜고 끌 수 있는 카드로 만들었어요. 아래에서 위로가 적용 순서예요. 「재생」을 누르면 각 층이 오른쪽 최종 항목에 자기 변경을 어떻게 칠하는지 볼 수 있어요. 세 번째 층에 집중하세요: 필드 하나만 썼는데, 아래 두 층이 애써 맞춰 둔 필드가 지워져요. 다 해 본 뒤 deep-merge 대조 모드로 바꿔, 같은 변경이 병합 의미론에서는 어떤 결말이 나는지 보세요.

의미론
합성된 최종 항목 · id: conversation-model
빈 항목 목록이에요. DSH 루트 설정은 빈 배열이고, 플러그인 트리는 patch가 층층이 칠해서 처음부터 만들어져요.
「재생」을 눌러 겹층을 시작하거나, 각 층 오른쪽 스위치로 먼저 참가 명단을 바꾸세요. 여기로 스크롤하면 한 번 자동 재생돼요.
수업용 시뮬레이션이에요: 항목 id·필드·파일명은 단순화했고, 겹층 순서와 교체 의미론은 apps/cli/src/profile-boot.ts 121–129행과 vendor/include/src/index.ts 58–128행에 대응해요. deep-merge 모드는 수업용 가정이며, DSH는 그 의미론을 구현하지 않았어요.
먼저 세 용어부터

결론부터요: DSH에는 크고 완전한 설정 파일이 없어요. 제품 형태는 patch 목록 더미를 쌓아 올린 것이고, 더미마다 소유자가 분명해요.

Bundle은 배포판 기본값이에요. npm 패키지이고, 실체는 패키지에 들어 있는 그 patch 목록이에요. 내장 세 개는 base(공유 코어), web-app(브라우저 표면), headless(일회성 작업 모드)예요.

Profile은 사용자의 한 벌 조립이에요. $DSH_HOME/profiles/<name> 아래 디렉터리이고, manifest에 쓸 bundle과 순서를 적어 두고, 옆에 사용자 patch 파일을 둬요. web이나 headless라는 이름을 처음 쓰면 템플릿이 자동 초기화돼요.

Patch는 변경의 최소 단위예요. patch 하나가 id로 대상 항목을 찾아 설정을 바꾸거나, 비활성화하거나, 새 항목을 insert해요. 트리 밖 플러그인도 여기서 들어와요: dsh plugin add로 profile에 넣으면, 그다음부터는 평범한 patch 한 층이에요.

출처: 내장 bundle 세 개와 package.json의 bundle 선언 방식은 packages/bundle/README.zh.md; profile 템플릿 자동 초기화는 apps/cli/src/profile.ts 114–117행.

설계 아이디어 1 · 설정 층마다 소유자가 있다

어떤 문제를 푸는가

반대 방식을 상상해 보세요: 큰 설정 파일 하나에 배포판 기본값, 이 조립의 커스텀, 개인 취향, 임시 실험이 전부 들어 있어요. 석 달 뒤 배포판을 올리면, 새 기본값과 옛 변경이 같은 파일에 뒤섞여서 어느 줄이 누구 것인지, 어느 줄을 건드려도 되는지 모르겠고, 업그레이드가 수동 대조가 돼요.

더 일상적인 사고도 있어요: 실험 모델을 잠깐 쓰려고 설정을 고쳤다가 되돌리는 걸 잊으면, 다음날 환경 전체가 실험 설정으로 돌아가요. 뿌리는 변경에 소유자가 없다는 거예요 — 누가 썼는지, 무엇과 같이 가는지, 언제 사라져야 하는지, 파일 하나로는 세 가지를 말할 수 없어요.

아이디어는 무엇인가

DSH는 설정을 네 층으로 나누고 층마다 소유자를 둡니다: Bundle은 배포판과, Profile은 이 조립과, Home은 이 기계와, --patch는 이번 명령과 함께 가요. 시작할 때 빈 배열을 향해 고정 순서로 patch 네 층을 차례로 칠하고, 늦게 적용한 쪽이 이겨요.

시작점은 정말 빈 배열이에요: 루트 설정 파일 내용이 []이고, 템플릿 주석도 이 파일은 건드리지 말고 patch 파일을 고치라고 써 있어요. 실제 내용은 전부 patch insert로 들어오니, 최종 플러그인 트리의 설정 한 줄마다 어느 층에서 왔는지 답할 수 있어요.

patch 네 층을 1부터 4까지 차례로 적용, 늦게 적용한 쪽이 이김 1 · Bundle 층 배포판 기본값, dsh 설치와 함께 2 · Profile 사용자 층 이 조립과 함께 3 · Home 사용자 층 전 기기 취향, 모든 profile에 적용 4 · --patch 덮어쓰기 층 일회성 실험, 명령과 함께 빈 배열에 순서대로 적용 저장소 유일의 patch 알고리즘 최종 플러그인 트리 boot() 마운트 실행 dsh --dump-config 같은 알고리즘으로 오프라인 합성
같은 patch 더미, 같은 알고리즘이면 마운트와 dump 결과가 어긋날 수 없어요.

사용자 patch 두 층의 역할도 정교해요: Profile 층은 이 조립과 함께 가고, Home 층은 기계 로컬 취향이라 모든 profile에 적용되므로 Profile 뒤에 두어 눌러요. 같은 id를 고치면 승자는 Home 쪽이에요. --patch는 맨 마지막에 두고, 파일에 안 남기고 싶은 일회성 실험에 쓰며, 여러 번 넘겨도 되고 명령줄 순서로 적용돼요. 긴 세션에서는 사용자 층 두 파일도 watch되어, 고쳐서 저장하면 실행 중인 트리가 같은 층 순서로 다시 조립돼요.

층 순서는 소스에서 배열 리터럴 하나예요: launcher가 조합된 profile을 patch 배열로 펼치고, 배열 순서가 곧 적용 순서예요. 이 함수는 짧은데 명언처럼 읽혀요 — 네 층의 선후가 조건 분기 없이 배열 하나에 박혀 있음을 증명해요:

apps/cli/src/profile-boot.ts121–129행
/** The full patch stack of one composed profile, in application order. */
function allPatches(composed: ComposedProfile): PatchOptions[] {
  return [
    ...composed.bundlePatches,
    ...composed.profile.patches,
    ...composed.homePatches,
    ...composed.overlays,
  ]
}
소스 스냅샷 안내: 로컬 저장소 deepseek-harness-master 기준, 확인 파일 apps/cli/src/profile-boot.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.

출처: 루트 설정이 빈 배열인 것과 템플릿 주석은 apps/cli/src/profile-boot.ts 60–64행; Home이 Profile을 누르는 이유는 packages/boot/app-boot/README.zh.md 43행; --patch 반복 전달은 apps/cli/src/args.ts 132행; 핫 리로드는 같은 패키지 watchUserPatches.

왜 오래가는가

층별 덮어쓰기는 설정 시스템의 통칙예요: CSS 캐스케이드, systemd drop-in 디렉터리, 에디터에서 사용자 설정이 기본을 누르는 것 — 모두 같은 구조예요. 제품 하나를 배포 측·팀·개인·단발 명령 네 역할이 동시에 고치면 층이 갈라져야 하고, 아니면 업그레이드와 롤백에 손댈 곳이 없어요. harness 전체를 다른 언어로 다시 써도 이 네 층은 그대로예요.

설계 아이디어 2 · 맞히면 통째 교체

어떤 문제를 푸는가

겹층에는 결정이 하나 더 남아요: patch 두 층이 같은 항목을 고치면 어떻게 할까요? 직관적 답은 deep-merge, 필드 단위 병합이에요. 위층은 바꿀 필드만 쓰고 나머지는 자동 유지되니 쓰기는 편해요.

편함의 대가는 둘이에요. 첫째, 병합은 삭제를 표현 못 해요: 아래층이 필드를 넣었고 위층이 지우고 싶어도 merge 의미론에는 그 동작이 없어, 다른 값으로 덮을 수밖에 없어요. 둘째, 최종 결과가 어떤 파일의 글자 그대로와도 안 맞아, 설정을 추적하려면 머릿속에서 모든 층의 병합 알고리즘을 돌린 뒤에야 지금 뭐가 적용되는지 알 수 있어요.

아이디어는 무엇인가

DSH는 통째 교체를 골랐어요. patch가 id로 대상을 맞히면, id를 뺀 모든 최상위 키를 바로 대입해요. config도 최상위 키라서 옛 config 객체가 통째로 바뀌고, 안 필드는 하나도 안 남아요. 필드 하나만 바꿔도 남길 필드를 다시 써야 해요. 공식 문서가 밝힌 알려진 제한이고, 원문은 “profile 덮어쓰기는 남겨야 할 번들 필드를 다시 서술해야 한다”예요.

그래서 가장 밟기 쉬운 반직관 결과가 데모 3층의 그 장면이에요: Home이 model 하나만 썼는데, 아래 두 층이 맞춰 둔 provider와 temperature가 지워지고, 경고도 없이 결과만 틀려요. 행동 경계 세 가지도 기억하세요: 없는 id를 가리키면 경고만 하고 에러는 안 내며 stderr 한 줄 뒤 건너뛰어요; patch 파일이 비었거나 주석만이면 바로 예외를 던져요 — 파싱 결과가 리스트가 아니니까요; 어떤 층이 아무 일도 안 하게 하려면 []를 쓰세요.

교체 의미론의 보상은 예측 가능성이에요. 이 알고리즘은 저장소에 하나뿐이에요: 마운트가 쓰고, dsh --dump-config의 오프라인 합성도 써서, dump와 실제 기동이 어긋날 수 없어요. 입력은 절대 변이되지 않고 결과는 항상 깊은 복사라서, 핫 리로드로 patch 하나를 빼도 정말 되돌아가고 예전 값이 캐시에 구워지지 않아요. 보이는 대로 고칠 지도도 있어요: docs/config-catalog.zh.md, 3152행 생성 문서로 로드 가능한 패키지마다 config 타입을 그대로 나열해요. patch가 쓸 수 있는 키를 알려면 그 목록을 보면 돼요.

dump에서 보이는 것이, 설정을 고쳐서 얻는 것이에요.

출처: 교체 의미론(최상위 키를 하나씩 대입)은 vendor/include/src/index.ts 110–124행, 유일 알고리즘과 불변 입력 약속은 같은 파일 JSDoc 43–52행; 통째 교체 공식 설명은 packages/boot/app-boot/README.zh.md 60행.

왜 오래가는가

선언적 설정의 오래된 선택지예요: merge의 편함인가, replace의 가독성인가. 교체 의미론은 항목마다 마지막 저자를 유일하게 두고, 문제 나면 마지막 patch만 찾으면 글자 그대로가 적용 내용이에요; 대가는 쓸 때 다시 서술하는 거예요. DSH는 예측 가능성 쪽에 장부를 맞췄어요. 여러 쪽이 겹칠 수 있고 사용자가 스스로 추적해야 하는 설정 시스템이라면 이 문제는 구현 언어와 무관하게 있어요.

가로 비교 · 세 제품이 설정을 어떻게 층으로 나누나

DeepSeek Harness

플러그인 항목 단위 · 통째 교체

층의 단위는 플러그인 항목이에요: patch 하나가 config 한 줄을 통째로 바꿉니다. Bundle부터 --patch까지 네 층 순서는 고정이고, 루트 설정은 빈 배열이라 모든 내용은 어느 층으로든 거슬러 갈 수 있어요.

깊은 능력을 바꾸는 일은 항목 한 줄을 바꾸는 일과 같아요: compaction 플러그인 config를 통째 교체하거나, 심지어 서드파티 구현을 insert해요.

Claude Code

설정 필드 단위 · 소스 다섯 층

SETTING_SOURCES는 다섯 층이에요: userSettings, projectSettings, localSettings, flagSettings, policySettings — 뒤일수록 큼(settings/constants.ts 7–22행). 관리자 policy 층이 항상 맨 위를 누르고, managed 소스에서만 먹는 필드도 있어요.

층으로 나누는 대상은 설정 필드이고, 바꾸는 건 동작 파라미터예요. 플러그인 트리 자체는 설정 테이블에 올라와 있지 않아요.

Grok Build

TOML 필드 단위 · deep merge

xai-grok-config는 deep_merge_toml로 재귀 병합해요(loader.rs 415–426행): 테이블 병합, 배열 교체, 위층 필드가 이겨요. 층 순서는 system_managed, managed, user이고, requirements와 MDM 관리 층이 마지막에 눌러요(loader.rs 234–248행).

DSH 데모의 deep-merge 대조를 골랐어요: 필드 하나 바꿀 때 다시 쓸 필요는 없지만, 위층이 아래층 필드를 지울 수는 없어요.

비교 초점은 두 곳이에요. 첫째 병합 의미론: Grok과 Claude Code는 필드 단위 병합이라 쓰기는 편하고, DSH는 항목 단위 교체라 쓰기는 장황한 대신 dump와 파일 글자가 맞는 예측 가능성을 얻어요. 둘째 층 대상: 저 둘은 설정 필드를 나누고, DSH는 플러그인 트리 자체를 나누니 사용자가 닿는 깊이가 달라요 — insert patch 하나로 서드파티 compaction을 메인 루프에 붙일 수 있고, 앞 두 제품 설정 체계에는 대응물이 없어요. Claude Code에만 있는 것도 말해야 해요: 관리자 정책 층과 managed 소스에서만 먹는 잠금 필드. DSH에는 아직 동등 메커니즘이 없고, 이 항은 공개 자료 기준이에요.

수업 실습
01

두 층 충돌을 손으로 따라가기

Profile 층에 id: conversation-model, config: { provider: deepseek, model: v3.2, temperature: 0.2 }, Home 층에 id: conversation-model, config: { temperature: 0 }를 써요.

문제 1: 통째 교체 의미론으로 최종 항목의 config를 쓰고, 어떤 필드가 사라졌는지·경고가 있는지 짚어 보세요.

문제 2: 두 patch가 앉은 층을 바꾸면 결과는 뭐가 되나요?

문제 3: dsh --dump-config 아이디어로, 기동하지 않고 답을 어떻게 검증할지 설명해 보세요.

Takeaway: DSH 설정은 빈 배열 위에 patch 네 층을 칠한 것이고, 순서는 Bundle, Profile, Home, --patch이며 늦게 적용한 쪽이 이겨요. 같은 id를 맞히면 config를 통째 교체하고 필드 병합은 하지 않으니, 남길 필드는 다시 써야 해요. 같은 알고리즘이 마운트와 dump를 모두 다루니 --dump-config에서 보이는 것이 설정을 고쳐서 얻는 것이에요.