2. 기술 스택과 규율

스택

2026년 6월 3일 첫 문서의 권장 스택은 다음과 같았다. macOS 호스트·코어·렌더러의 큰 축은 유지됐고, 웹과 다른 플랫폼은 이후 별도 구현으로 확장됐다.

호스트 앱: Swift/AppKit
코어:      Zig
렌더러:    Metal
터미널 코어: Maru 자체 clean-room VT core
장기 웹:   Wasm + WebGPU backend, native 초기 renderer와 별도

핵심은 Swift/AppKit이 얇은 호스트 레이어만 맡는다는 것이다. 창·포커스·IME·메뉴처럼 OS가 쥐고 있는 것만 Swift가 하고, 화면에 무엇을 그릴지는 Zig 코어가 정한다. SwiftUI 레이아웃을 핵심으로 삼지 않는다는 문장이 첫 문서에 명시돼 있다.

9월 26일 공개 main의 구현은 초기 계획과 구분해서 읽어야 한다.

영역현재 구현
macOSSwift/AppKit 호스트, Zig 코어, CoreText 글자 처리와 Metal 렌더링
WindowsZig/Win32 호스트, DirectWrite 글자 처리, D3D11·DXGI 표시 경로
iOS·Android공통 Zig 코어, iOS UIKit·Metal·CoreText, Android NativeActivity·Vulkan·Java IME 연결
브라우저 터미널Zig 코어를 WASM으로 컴파일하고 TypeScript API·워커·Canvas 2D 렌더러를 결합
네이티브 편집기rope 편집 버퍼, tree-sitter 구문 분석, 외부 LSP 서버와 연결

브라우저의 현재 렌더러는 초기 표의 장기 구상인 WebGPU와 다르다. @maru/core와 React·Vue·Svelte·Lit 연결 코드가 저장소에 있고, CanvasRenderer가 getContext("2d")를 쓴다. 이는 저장소 구현에 대한 설명이며 npm 배포 여부를 뜻하지 않는다. 브라우저 패키지, Canvas 렌더러, Windows 계약, 모바일 계약, 네이티브 편집기.

앱 안의 Chromium 웹 패널은 또 다른 축이다. 9월 26일 확인 시점에 통합 PR #3867이 열려 있어, 개발 브랜치의 CEF 작업을 위 main의 구현에 포함하지 않았다.

경계: 코어는 PTY를 모른다

docs/architecture.md의 그림이 이 프로젝트의 성격을 가장 잘 보여준다. TerminalCore는 PTY도, 렌더러도, 플랫폼도 직접 알지 않는다.

사용자 입력 → App Host(창/포커스/IME/메뉴)
           → KeyBindingResolver (AppAction / TerminalInput 분류)
           → SurfaceRuntime (live 연결, 저장 안 함)
           → PtySession Facade  ⇄  Surface(TerminalCore + metadata)
           → RenderSnapshot → Renderer(Metal) → 화면

여기서 두 가지가 계약으로 못박혀 있다.

  1. Surface는 live handle을 저장하지 않는다. 복구 가능한 터미널 상태와 메타데이터만 갖는다. 실행 중 연결은 app layer의 SurfaceRuntime이 쥔다. 덕분에 워크스페이스 복원이 살아 있는 프로세스 핸들을 저장하지 않아도 되고, 테스트가 PTY 없이 코어를 검증할 수 있다.
  2. 금지된 의존성이 그림에 함께 그려져 있다. TerminalCore와 Renderer가 PTY 핸들에 직접 닿는 것, 플러그인 경계가 코어의 private 저장소에 닿는 것은 금지다.

이 경계는 나중에 실제로 값을 했다. 6월에 세션 모델을 OS 중립 src/session/으로 뽑아낼 때도, 7월에 터미널 런타임 수명을 GUI 프로세스 밖으로 옮길 때도, 8월에 코어를 iOS·Android 호스트에 붙일 때도 같은 경계선을 따라 잘렸다.

검증: 오라클과 골든

clean-room 구현이라 "맞게 만들었는가"를 스스로 증명해야 한다. Maru는 세 가지를 쓴다.

  • 외부 오라클 — 참조 터미널의 동작과 비교해 VT 준수를 판정한다.
  • 골든 캡처 — 실제 렌더 결과를 이미지로 고정한다. 8월에는 골든이 환경(UI 언어)에 흔들리던 것을 잡고 캡처 언어를 고정했다.
  • 성능 예산 하네스 — 시작 첫 주에 이미 붙였다. 프레임·메모리 예산을 넘으면 CI가 잡는다.

규율: 문서가 먼저, 그리고 적대적 검증

Maru 커밋 로그에는 docs(...)와 feat(...) 작업이 반복해서 교차한다. 이 규율은 작업 세션에서 직접 지시한 것이고, 그 지시가 프로젝트 규칙 문서가 됐다.

"그리고 문서에 작성된 내용 검증이 가능하다면 다 검증해서 확실하게 박아주세요 추론이나 계획으로만 하지말고" — 2026-06-29

"좋아요 가능하면 다 TDD로 하고 페이즈를 더 늘리더라도, 더 적은 단위로 확실하게 하는 형태로 했으면 좋곘는데요" — 2026-06-29

"그리고 문서에 각 페이즈 시작전에, 다시 사용자에게 무엇을 할지 설명하고, 이전 페이즈에 무너진게 없는지 검증한다를 적어주세요" — 2026-06-29

"구현할때마다 각각의 파트를 각각의 관점(유지보수, 소스오브트루쓰, 성능, 보안)에서 서브에이전트로 적대적 검증을 하면서 진행하세요" — 2026-07-17

작업 방식이 대체로 이 순서다.

  1. 계약 문서를 먼저 쓴다(doc-first). 무엇을 하지 않을지, 무엇이 단일 출처인지 정한다.
  2. 그 계약을 코드로 구현한다.
  3. 적대적 검증을 돌린다 — 자기 설계를 깨뜨리려 드는 검증을 여러 라운드 반복한다. 커밋 메시지에 "적대적 검증 7회차", "R3~R10", "3차 전수조사" 같은 표기가 그대로 남아 있다.
  4. 검증이 설계를 뒤집으면 문서를 고치고 되돌린다. 7월 27일에는 검증 결과로 PanelKind 확장과 EntryId 삭제를 둘 다 철회했다.

문서가 코드보다 뒤처지는 것을 막기 위한 장치도 따로 있다. 스키마↔문서 doc-drift 가드, 문서 간 링크·앵커·절 참조를 검사하는 CI 게이트, 계획 문서가 인덱스에서 도달하는지 보는 게이트가 모두 CI에 걸려 있다.

레퍼런스를 다루는 방식

레퍼런스와 관련해서는 당시의 개별 요청과 현재 저장소 규칙을 구분할 필요가 있다.

7월 작업에는 특정 레퍼런스의 이름을 문서나 PR에 적지 말라는 요청이 있었다.

"문서에 레퍼런스 삼는거 적을필욘 없는데 그냥 레퍼런스 폴더에 있으니 참고해서 결정된 내용을 쓰면되는겁니다" — 2026-07-21

"추천대로 D로 가시죠. doc-first로 가되 레퍼런스가 해당 라이브러리라는건 문서나 pr에 언급하지마세요." — 2026-07-24

이를 모든 출처를 남기지 않는 규칙으로 일반화할 수는 없다. 현재 docs/references.md는 공개 명세와 오라클의 출처를 기록하고, VT 변경 PR에는 구현한 명세 절을 인용하도록 한다. 플랫폼·렌더러 작업도 공개 문서에서 유도했는지, 독립 설계인지, 동작 비교를 했는지 근거를 남긴다. 외부 코드의 구조나 제어 흐름을 옮기지 않는 규칙과 출처를 적는 일은 함께 적용된다. 레퍼런스와 공개 명세.

코드를 열어 보기 전에 라이선스부터 확인한다는 요청도 있었다.

"https://github.com/tmux/tmux?tab=ISC-1-ov-file 티먹스 라이센스는 코드를 봐도 되는건가요?" — 2026-07-20

규율이 남긴 흔적

  • digest 원장 — 경계를 넘는 파일이 바뀌면 digest가 움직이고, 리베이스마다 원장을 다시 맞춘다. 커밋 로그의 fix(ci): 리베이스 뒤 digest를 맞춘다가 그 흔적이다.
  • 주석이 아니라 게이트 — "cwd는 한 축을 쓴다" 같은 규율을 주석에 적어 두는 대신 CI가 보게 옮긴다(8월 12일).
  • 리베이스로 히스토리 정리 — "그리고 메인기준으로 머지하셨는데 머지로하지말고 리베이스로 히스토리 정리해주세요"(6/29). 당시 작업에서 커밋을 정리하는 방식으로 남은 요청이다.
  • PR 단위 작업 — 한 PR이 한 슬라이스를 맡고, 이름에 P3d-②b, CR2e, SV1c, I3a 슬라이스 4 같은 계획 좌표가 붙는다. 머지 전에는 /code-review max를 돌린다("머지하고 P3-e2b 이어서 진행하기전에 여턔까지 한거 /code-review max 한번 해주세요", 7/21).