Profile / Bundle / Patch: 사용자가 제품을 어디까지 바꿀 수 있나
소스를 건드리지 않고도 깊은 능력을 바꿀 수 있어요: 설정은 빈 배열 위에 patch 네 층을 쌓고, 늦게 적용한 쪽이 이기고, 같은 항목을 맞히면 통째로 교체해요.
--patch(일회성 실험)로 나뉘는지, 층끼리 우선순위는 어떻게 잡히는지; patch가 같은 항목을 맞힐 때 왜 통째 교체인지, 그 선택이 어떤 반직관적 결과를 만드는지; 그리고 dsh --dump-config가 최종 적용 설정을 어떻게 펼쳐 보여 주는지.
아래 샌드박스는 설정 네 층을 켜고 끌 수 있는 카드로 만들었어요. 아래에서 위로가 적용 순서예요. 「재생」을 누르면 각 층이 오른쪽 최종 항목에 자기 변경을 어떻게 칠하는지 볼 수 있어요. 세 번째 층에 집중하세요: 필드 하나만 썼는데, 아래 두 층이 애써 맞춰 둔 필드가 지워져요. 다 해 본 뒤 deep-merge 대조 모드로 바꿔, 같은 변경이 병합 의미론에서는 어떤 결말이 나는지 보세요.
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행.
어떤 문제를 푸는가
반대 방식을 상상해 보세요: 큰 설정 파일 하나에 배포판 기본값, 이 조립의 커스텀, 개인 취향, 임시 실험이 전부 들어 있어요. 석 달 뒤 배포판을 올리면, 새 기본값과 옛 변경이 같은 파일에 뒤섞여서 어느 줄이 누구 것인지, 어느 줄을 건드려도 되는지 모르겠고, 업그레이드가 수동 대조가 돼요.
더 일상적인 사고도 있어요: 실험 모델을 잠깐 쓰려고 설정을 고쳤다가 되돌리는 걸 잊으면, 다음날 환경 전체가 실험 설정으로 돌아가요. 뿌리는 변경에 소유자가 없다는 거예요 — 누가 썼는지, 무엇과 같이 가는지, 언제 사라져야 하는지, 파일 하나로는 세 가지를 말할 수 없어요.
아이디어는 무엇인가
DSH는 설정을 네 층으로 나누고 층마다 소유자를 둡니다: Bundle은 배포판과, Profile은 이 조립과, Home은 이 기계와, --patch는 이번 명령과 함께 가요. 시작할 때 빈 배열을 향해 고정 순서로 patch 네 층을 차례로 칠하고, 늦게 적용한 쪽이 이겨요.
시작점은 정말 빈 배열이에요: 루트 설정 파일 내용이 []이고, 템플릿 주석도 이 파일은 건드리지 말고 patch 파일을 고치라고 써 있어요. 실제 내용은 전부 patch insert로 들어오니, 최종 플러그인 트리의 설정 한 줄마다 어느 층에서 왔는지 답할 수 있어요.
사용자 patch 두 층의 역할도 정교해요: Profile 층은 이 조립과 함께 가고, Home 층은 기계 로컬 취향이라 모든 profile에 적용되므로 Profile 뒤에 두어 눌러요. 같은 id를 고치면 승자는 Home 쪽이에요. --patch는 맨 마지막에 두고, 파일에 안 남기고 싶은 일회성 실험에 쓰며, 여러 번 넘겨도 되고 명령줄 순서로 적용돼요. 긴 세션에서는 사용자 층 두 파일도 watch되어, 고쳐서 저장하면 실행 중인 트리가 같은 층 순서로 다시 조립돼요.
층 순서는 소스에서 배열 리터럴 하나예요: launcher가 조합된 profile을 patch 배열로 펼치고, 배열 순서가 곧 적용 순서예요. 이 함수는 짧은데 명언처럼 읽혀요 — 네 층의 선후가 조건 분기 없이 배열 하나에 박혀 있음을 증명해요:
/** 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,
]
}
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 전체를 다른 언어로 다시 써도 이 네 층은 그대로예요.
어떤 문제를 푸는가
겹층에는 결정이 하나 더 남아요: 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가 쓸 수 있는 키를 알려면 그 목록을 보면 돼요.
출처: 교체 의미론(최상위 키를 하나씩 대입)은 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 mergexai-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에는 아직 동등 메커니즘이 없고, 이 항은 공개 자료 기준이에요.
두 층 충돌을 손으로 따라가기
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 아이디어로, 기동하지 않고 답을 어떻게 검증할지 설명해 보세요.