파이프라인 4단계(Design) 산출물 · 2026-09-16 · rev.4(rev.2 = Architect 리뷰 AR-01~21 / rev.3 = UX UR-02 · UR-11 · 흐름 순열 E-C1 · E-I1 · E-I25 / rev.4 = Architect 2라운드 R2-01~06 필수 + P-01~06 권고 반영)
근거: docs/seed-spec-fullstack-code-editor.md v1.1.1 LOCKED(hash 79991d04) · docs/prd-fullstack-code-editor.md FR-1xx~7xx · NFR-01~06 · U 표 · docs/bdd-fullstack-code-editor.md(v1.1 · 83 시나리오) · docs/planning-inputs-fullstack-code-editor.md §5(코드 앵커) · §7(패널 지적) · 상위 docs/architecture-cocode.md(§1.4 C1~C7 · DD-02a · DD-06 · DD-15 · DD-23a · §5.2) · docs/package-placement-cocode.md · dependency_layers.yaml · 리뷰 scratchpad/design-review-architect.md
0. 이 문서가 서 있는 지반
| 항목 |
상태 |
근거 |
| Seed Spec |
v1.1.1 LOCKED — 확정 계약. 단 A-01 은 잠정 결정 |
seed Metadata · D-014 |
| 이 문서의 지위 |
Seed · PRD 를 구현 가능하게 만드는 층. 경계를 넓히거나 좁히지 않는다. 어긋나면 §11 에 적는다 |
상위 architecture §0 과 같은 원칙 |
| 상위 아키텍처 |
docs/architecture-cocode.md 의 결정(DD-00~DD-26)을 그대로 전제한다. 이 문서는 에디터 모듈의 결정(ED-nn)만 더한다 |
상위 문서 §8 |
| 하드 의존 |
D-013(롤백 사전검사 체인 불연속 — 상위 core/workspace 변경) · D-018(기존 폴더 열기 — 상위 워크스페이스 생명주기) |
planning-inputs §1.1 |
표기: [경계] Seed/PRD 문면 · [도출] 상위 규약 · 실측에서 연역 · [ED-nn] 이 문서가 새로 내리는 결정(§9 에 전량) · 수치는 출처가 있을 때만.
1. 아키텍처 드라이버
| # |
드라이버 |
출처 |
설계 귀결 |
| AD-1 |
10만 줄 파일 · 탭 N개에서 응답성(NFR-01) — 고빈도 이벤트는 Bloc 미통과 |
상위 AC-15 · DD-06 · A-03 |
버퍼 content · 터미널 출력은 컨트롤러/버퍼 객체가 보유, Bloc 은 저빈도 요약만(§3 ED-02) |
| AD-2 |
앱 명령은 단일 런처(C-05) · dart:io 쓰기는 허용 파일뿐(DD-15) · 환경변수 화이트리스트는 사람 터미널에도(DD-23a) |
Seed C-05 · 상위 §5.2 |
PTY 기동도 런처 메서드 하나(startPty)로 — spawn 구현은 의존 역전으로 주입(§4.6 ED-06) · 사람 편집 저장 · 실행 구성 영속 파일을 허용 디렉터리로 신설(§4.3 · §4.5) |
| AD-3 |
계층 규칙 C1~C7 · 래칫 · 계약 완결성(DD-02a) · 3층(Domain/Data/Presentation) |
상위 §1.4 · §2 · CLAUDE.md BLoC-UseCase 패턴 |
신설 패키지 0, 계약 1종 신설(DiagnosticsSink, 12→13) · L2↔L2 간선 0 · UseCase 층 6종 신설(§3) |
| AD-4 |
3-OS(상위 C-01) · Flutter stable(D-025) |
Seed 상속 |
윈도잉 API 미사용 · PTY 는 FFI(stable 동작) · 순수 Dart 신설 코드는 3-OS dart test |
| AD-5 |
사람 편집과 에이전트 편집의 분리(C-03) — 스냅샷 없음 · 리스 없음 · 체인 불연속 충돌 |
Seed C-03 · AC-10 · 22 · 13 |
사람 저장 경로는 게이트의 에이전트 경로와 다른 파일(§4.3) · 불연속 판정은 상위 사전검사 확장(§4.8, D-013) |
| AD-6 |
한글 IME 분리 판정(C-04) |
Seed C-04 · 상위 D-069 |
새 입력 표면 5종을 probe.ac16-ime-replay 매트릭스에 추가(§6) |
2. 패키지 배치 — 신설 패키지 0, 확장 7 + 앱
[ED-01] 신설 패키지를 만들지 않는다. 상위 DD-00 의 K1~K4 기준으로 심사한 결과, 이번 범위의 모든 코드는 기존 12종의 책임 안에 든다. K2(교체 가능 외부 부품) 후보인 PTY 구현은 terminal 안의 PtySpawner 구현으로 두고, 기동 정책과 호출 지점은 toolchain 런처 하나에 남긴다(ED-06).
| 패키지 |
층 |
이번 범위에서 추가되는 것 |
새 워크스페이스 의존 |
core (L0) |
엔티티 DocumentBuffer(순수) · RunConfiguration · TerminalSession · 계약 DiagnosticsSink(13번째, contracts.dart + contract_registry.dart:12 목록 갱신) · ProcessLauncher 메서드 추가(startPty) · LaunchedProcess.terminate({grace}) · 협력 타입 파일 pty_types.dart(PtyLaunchRequest · PtyHandle · PtySpawner) · diagnostics_types.dart(Diagnostic · LanguageServerStatus) · 포트 usecase/editor/ports.dart(HumanEditStore · WorkspaceSearchPort — 계약 목록 밖 협력 타입, ED-19) · 순수 함수 domain/content_hash.dart(contentHashOf(Uint8List) → String, ED-18) · 유즈케이스 6종(§3) · RollbackConflict.chainDiscontinuity(D-013, 상위 Story) |
없음 |
|
[ED-18] 경로 키 · 해시는 L0/L2 에서 String 이다(R2-02). PathKey · ContentHash 는 workspace(L1) 타입이라 core(L0) · editor(L2, pubspec 에 workspace 없음)가 참조할 수 없다. 상위 관례(EditSnapshot.path · baseContentHash 가 String)대로 L0/L2 는 정규화 경로 문자열 pathKey: String 과 해시 문자열을 쓰고, PathKey/ContentHash 로의 변환 · 검증은 workspace 경계(HumanEditWriter · WorkspaceSearch)에서 한다. 해시 알고리즘은 core.contentHashOf 가 상위 DD-10 과 같은 값을 내야 하며 workspace 테스트가 두 함수의 동치(parity) 를 단언한다. PathKey 의 core 승격은 하지 않는다(상위 Story 를 만들지 않는 쪽).
| editor (L2) | EditorDocument(= DocumentBuffer + CodeLineEditingController) · DocumentBufferRegistry · EditorDocumentSource implements TextDocumentSource · CocodeDiagnosticsSink implements DiagnosticsSink · LanguageIdResolver(비-Dart re_highlight 매핑) | 없음 |
| terminal (L2) | FlutterPtySpawner implements core.PtySpawner(Pty.start 텍스트가 있는 유일한 파일 — 허용 목록 등재) · PtyTerminalBackend implements CocodeTerminalBackend | flutter_pty2(스파이크 통과 시). toolchain 의존은 필요 없다(AR-15 — spawner 는 core 타입만 구현) |
| lsp (L2) | publishDiagnostics 를 DiagnosticsSink 로 전달 — 생성자 선택 인자 diagnostics:(기본 NullDiagnosticsSink, 폐기 카운터 유지 — 기존 47 테스트 · 호출처 무영향, AR-14) | 없음 |
| toolchain (L1) | ToolchainProcessLauncher.startPty(PtyLaunchRequest) — 정책(프로파일 · DD-23a · cwd · 셸 인자) 적용 후 주입된 PtySpawner 호출 · terminate({grace}) 구현 · 언어 서버 · git check-ignore · 버전 조회는 기존 run/start | 없음 — 순수 Dart 유지(flutter_pty2 미의존) |
| workspace (L1) | HumanEditWriter(사람 저장 — 쓰기 허용 디렉터리 human_edit/ 신설) · FileChangeWatcher · WorkspaceScanner + IgnoreMatcher · WorkspaceSearch(isolate) · ProjectDirectoryDetector · RunConfigurationStore(.cocode/editor/run_configurations.json — 쓰기 허용 디렉터리 editor/ 신설, AR-11) | yaml(pubspec 판정) |
| ui (L0) | 신설 4종 CocodeTaskFileBanner · CocodeDirtyDispositionDialog · CocodeProblemsList · CocodeSessionTabBar(전부 뷰모델 입력, C4, 위젯북 use-case 동반) + 기존 CocodeStatusBar additive 변경 trailingCells: List<Widget>(기본 빈 목록 — 상위 4칸 계약 불변, 위젯북 use-case 갱신, UR-02) | 없음 |
| app/cocode (L3) | lib/feature/{shell,editor,search,language,run,terminal,crossing}/ — Bloc(저빈도) · 페이지 · 워크스페이스 스코프 컨테이너(§4.9) · 계약 등록 · 메뉴/팔레트 | editor · terminal · lsp · toolchain · desktop_platform |
계층 검증: 새 간선은 app → 5종(L3 sink) 뿐. terminal → toolchain 은 rev.1 에서 계획했으나 ED-06 의 의존 역전으로 불필요해졌다(AR-15). L2↔L2 간선 0(C1 · C3) · ui → core 0(C4) · toolchain → lsp 0(C5). 래칫 지표는 변하지 않는다.
app/cocode (L3) feature/{shell,editor,search,language,run,terminal,crossing} + 워크스페이스 스코프 컨테이너
┌──────────┬───────────┬──────────┐
editor terminal lsp (L2) ← terminal 은 core.PtySpawner 를 구현할 뿐(L1 의존 없음)
└──────────┴─────┬─────┴──────────┘
workspace toolchain (L1) ← 쓰기 허용 디렉터리 2개 신설 · startPty/terminate 구현
└──────┬──────┘
core ui desktop_platform (L0) ← 계약 13종 · 엔티티 3종 · 협력 타입 2파일 · 유즈케이스 6종
3. 레이어 구성 (Clean Architecture × 패키지 계층 — 별개 축)
| 레이어 |
이번 범위의 거처 |
예 |
| Domain — Entity |
core/lib/src/entity/ |
DocumentBuffer · RunConfiguration · TerminalSession |
| Domain — Contract |
core/lib/src/contract/contracts.dart (+ 협력 타입 pty_types.dart · diagnostics_types.dart, AR-12) |
DiagnosticsSink(신설) · TextDocumentSource · ProcessLauncher(+startPty) · LaunchedProcess(+terminate) |
| Domain — UseCase [ED-03] |
core/lib/src/usecase/editor/ |
OpenDocumentUseCase · SaveDocumentUseCase · ReloadDocumentUseCase · SearchWorkspaceUseCase · StartRunConfigurationUseCase · StopSessionUseCase — Bloc 은 이들을 optional constructor injection(1~7개 개별 파라미터, CLAUDE.md 패턴)으로 받는다. 유즈케이스는 계약(HumanEditStore · WorkspaceSearchPort · ProcessLauncher …)만 의존한다 |
| Data — 구현 |
workspace · toolchain · editor(source/sink) · terminal(backend/spawner) |
HumanEditWriter · RunConfigurationStore · PtyTerminalBackend · FlutterPtySpawner |
| Presentation — BlocSignal |
app/cocode/lib/feature/*/bloc/ |
EditorSessionBloc · TerminalSessionsBloc · RunConfigBloc · ProblemsBloc — 저빈도만 |
| Presentation — Widget |
ui(제품 어휘 없는 위젯) + app/cocode/lib/feature/*/page |
CocodeSessionTabBar ↔ 앱의 세션 매핑 |
- 저장소 인터페이스 명명(§11 기록): CLAUDE.md 는
I 접두어(IHomeRepository)를 규정하나 상위 coco 계약 12종은 접두어 없이 명명됐다(SnapshotStore · WorkspaceFileGate). 이 모듈은 상위 관례를 따른다 — 같은 파일(contracts.dart)에 두 표기가 섞이는 것을 피하기 위해서다.
[ED-02] 고빈도 경계 (DD-06 확장 적용). check_high_frequency_event_isolation.py 는 editor · terminal 의 bloc_signals 의존 부재만 본다. 앱 레벨 레지스트리는 가드 밖이므로 이 문서가 규칙으로 고정한다 — DocumentBufferRegistry(editor) 와 TerminalSessionManager(앱) 는 ChangeNotifier/Stream 으로만 알리고, BlocSignal 상태에는 content · scrollback · 커서 · 스크롤을 넣지 않는다(허용: 열린 문서 목록 · dirty 집합 · 활성 탭 id · 세션 목록 · status · 진단 개수). 위반 감지는 app/cocode/test/src/feature/editor/editor_session_bloc_test.dart 가 상태 타입의 필드를 열거해 CodeLineEditingController/String content 부재를 단언한다.
4. 컴포넌트 설계 (FR 영역별)
4.1 문서 버퍼 · 탭 (FR-2xx)
core DocumentBuffer { pathKey, diskHash, dirty, languageId, externalChangePending } ← Seed §3 이름 그대로(AR-20)
OpenDocumentUseCase / SaveDocumentUseCase / ReloadDocumentUseCase (HumanEditStore 포트 의존 — `usecase/editor/ports.dart`, ED-19)
editor EditorDocument { buffer: DocumentBuffer, controller: CodeLineEditingController } — undo/redo 는 컨트롤러 내장(re_editor code_line.dart:155-309)
DocumentBufferRegistry Map<String /*pathKey*/, EditorDocument> — 불변식 (1) 유일성 · (2) 수명 · (3) 보호 · ChangeNotifier
EditorDocumentSource implements TextDocumentSource — didOpen/didChange 공급(FR-401)
app/cocode EditorSessionBloc({open, save, reload}: optional injection) state { openDocs, layout, dirtySet } (저빈도 — activeTab 은 layout 의 활성 그룹에서 읽는다, D-084)
EditorLayout 분할 트리 · 그룹{탭 순서 · 활성 탭} · 활성 그룹 — 경로 키와 그룹 번호만(ED-02)
EditorStackPage 그룹 하나의 탭 줄 + 본문(그룹마다 Monaco 표면 하나) · 탭 끌어 놓기 — CocodeEditorGroupsView 가 트리를 Resizable 로 그린다
- [ED-04] 버퍼 수명은 레지스트리가 소유한다. 탭은 정규화 경로 문자열(
pathKey: String, ED-18) 참조만 갖는다. 탭 닫기 → EditorSessionBloc.closeTab → dirty 면 CocodeDirtyDispositionDialog(저장 · 폐기 · 취소) → 처분 후에만 registry.release(pathKey). 워크스페이스 닫기 · 앱 종료 → registry.dirtyDocuments() 를 일괄 다이얼로그로 → 처분 전에는 진행하지 않는다. 앱 창 닫기 가로채기는 앱 창 레벨(WindowManager 에 close 훅이 없다 — desktop_platform 변경 불요, 앱이 Flutter 창 닫기 이벤트를 처리).
- dirty 판정:
buffer.dirty = contentHashOf(utf8(controller.text)) != diskHash — core 순수 함수(ED-18, 상위 DD-10 과 같은 알고리즘 · parity 테스트, package:crypto 의존을 core pubspec 에 명시 — P-07). 해시는 키 입력마다가 아니라 유휴 디바운스 시점에 계산하고 탭 배지는 그 결과를 따른다(P-08 — 전체 해시 비용을 고빈도 경로에서 뺀다; 디바운스 간격은 §10 미정 값). 저장 · 로드 시에만 diskHash 갱신.
- 저장(FR-204):
SaveDocumentUseCase(pathKey, bytes) → HumanEditStore.write(포트, ED-19) → 구현 HumanEditWriter(§4.3) → sealed 결과 HumanWriteApplied(diskHash) | HumanWriteFailed(reason)(AR-13) → Bloc 이 실패를 상태바 «저장 실패» + 본문 상단 Banner(.destructive) 로(UX-E-37, 토스트 없음). 열기 · 다시 읽기는 HumanEditStore.read(pathKey)(P-05).
4.2 파일 감시 · 외부 변경 (FR-205)
workspace FileChangeWatcher — 감시 범위는 두 종류(AR-09): ⓐ 열린 문서의 부모 디렉터리(Directory.watch) ⓑ 워크스페이스 메타 파일(.gitignore · pubspec.yaml · .cocode/editor/run_configurations.json)의 부모 디렉터리 — ⓑ 는 IgnoreMatcher · ProjectDirectoryDetector 캐시 무효화 전용. 이벤트 → FileChange{path, kind: modified|deleted|moved, newPath?}.
- 자체 저장 억제:
HumanEditWriter 가 쓰기 직전 expectedHashAfterWrite 를 등록하고, 감시 이벤트가 그 해시와 일치하면 무시(타이밍 창이 아니라 해시 대조 — flutter_ide §6-b4 교훈).
- 앱: modified + clean →
ReloadDocumentUseCase; modified + dirty → 배너 선택(ux-spec UX-E-04); deleted/moved → buffer.externalChangePending = true 표시, 다시 읽기 없음(FR-205).
- 감지 지연 상한(U-3)은 값을 정하지 않는다 — §10.
4.3 사람 편집 저장 경로 (FR-209 · AC-13) — [ED-05]
workspace/lib/src/human_edit/human_edit_writer.dart ← dart:io 쓰기 허용 디렉터리 신설(check_dart_io_write_allowlist.py ALLOWLIST 행 추가)
Future<HumanWriteResult> write(String pathKey, Uint8List bytes): ← 내부에서 PathKey 로 변환 · 검증(ED-18) · read(String pathKey) 도 같은 검증
1. 경로 검증 — realpath 해석 후 Workspace 루트 안(심볼릭 링크로 밖을 가리키면 거부) · 구분자/`..` 거부(NFR-04) · **`.cocode/**` 거부**(단 `.cocode/editor/**` 는 허용 — 실행 구성 JSON 편집) (AR-10)
2. 임시 파일 → rename 치환(상위 D-040: Windows 도 File.rename)
3. SnapshotStore · EditLockRegistry 를 호출하지 않는다 ← AC-13 가드: 이 디렉터리에서 두 계약 타입 참조 0건을 스캔
4. 결과 sealed: HumanWriteApplied(diskHash) | HumanWriteFailed(reason: OS 메시지 · 거부 사유 코드)
- 에이전트 경로(
WorkspaceFileGate)와 파일이 다르다 — 같은 게이트에 humanWrite 를 두면 "스냅샷을 남기지 않는 게이트 호출"이 생겨 C-02 가드가 흐려진다.
- 검색 치환(FR-306)도 이 경로다: 대상 파일을 버퍼로 열어(레지스트리 경유)
SaveDocumentUseCase 로 저장.
4.4 탐색 · 검색 (FR-3xx)
workspace WorkspaceScanner.children(dir) → Directory.list(비동기) + IgnoreMatcher.isIgnored(paths) 필터. [ED-06a] git 저장소이면 .gitignore 판정 정본은 git 이다 — 단 git 저장소인데 git 실행 파일이 없거나 .git 을 읽을 수 없으면(E-I1) 비-git 과 같은 루트 .gitignore 파일 규칙으로 후퇴하고 IgnoreMatcher.mode = fileRulesFallback 을 노출해 UI 가 «git 판정 불가» 를 표시한다. — IgnoreMatcher 는 ProcessLauncher.run('git', ['check-ignore', '--stdin', '-z'], profile: ED-12) 결과를 디렉터리 단위로 캐시하고, .gitignore 변경(§4.2 ⓑ) 시 무효화(상세 U-14). git 저장소가 아니면 Seed AC-01 의 파일 규칙 의무가 남으므로 루트 .gitignore 파일만 순수 Dart matcher 로 적용한다(하위 · 전역은 대상 아님 — 중첩 · 부정 패턴의 완전 재현은 git 에 위임; matcher 선택은 U-14). 파일도 없으면 제외 0(FR-302).
- 탐색기 드롭의 SSOT:
coco-de/coui Tree(#5081 · #5090 · #5104)가 행 렌더·안정 ID·가상 뷰포트·선택/확장 상태·범용 드래그 제스처와 자기 자신/자손으로 놓기 거부를 소유한다. 폴더 행은 해당 폴더, 파일 행 위 삽입선은 부모 폴더를 드롭 목적지로 보고한다. 파일 경로·확장자·권한이나 디스크 쓰기는 CoUI 에 들어가지 않는다. package/ui 의 CocodeFileTree 는 CocodeTreeOutline 을 CoreTreeNode 로 번역하고 파일 타입 아이콘·배지를 슬롯에 공급하는 얇은 어댑터다. UI 가 돌려주는 드롭 콜백은 의도일 뿐이다.
- 실파일 작업의 SSOT:
app/cocode 는 열린 버퍼(DocumentBufferRegistry)에 걸린 파일/폴더 이동을 거부하고, workspace 의 HumanEditWriter.move/importBytes 만 호출한다. 이 경로는 기존 사람 편집의 realpath·.cocode 검증을 재사용하고 목적지 점유를 거부하며 에이전트 Snapshot/리스는 만들지 않는다(C-03). OS 파일은 별도 업로드 상자 없이 Tree 행의 실제 목적지에 직접 드롭해 추가한다. 성공 후 WorkspaceScanner+IgnoreMatcher 로 펼친 자리만 다시 읽고, 실패는 탐색 패널의 지속 표시 Banner 로 남긴다.
SearchWorkspaceUseCase(query, regex, roots) → WorkspaceSearchPort(포트, ED-19) → 구현 WorkspaceSearch: Isolate.run 안에서 파일 순회(스캐너 무시 규칙 적용) → Stream<SearchHit{pathKey: String, line, column, preview}> + 취소 토큰. 정규식 컴파일 오류는 isolate 진입 전에 FormatException 으로 반환(BDD Feature 3 error). 인덱스는 두지 않는다(첫 릴리스, YAGNI).
- 결과 선택 →
OpenDocumentUseCase(pathKey) + 커서 이동.
4.5 실행 구성 (FR-5xx)
- 엔티티
RunConfiguration 은 core. 영속 RunConfigurationStore(workspace/lib/src/run_config/, .cocode/editor/run_configurations.json — [ED-07] .cocode/config/ 디렉터리는 생성까지 상위 #22 소유(cocode_layout.dart:49)이므로 이 모듈은 .cocode/editor/ 를 소유한다. 쓰기 허용 디렉터리 editor/ 신설). 두 쓰기 주체 규칙(UR-11): 프로그램 쓰기는 RunConfigurationStore 만 하고(사용자 JSON 편집은 HumanEditWriter 경로 — .cocode/editor/** 허용), store 는 파일 변경 감시(§4.2 ⓑ 범위에 이 파일 포함)로 재읽기하며, 사용자가 그 파일을 dirty 로 열어 둔 동안 store 가 써야 하면 외부 변경 흐름(배너 선택)으로 처리한다. 파싱 실패(E-I25): store 는 빈 목록 + RunConfigLoadFailed(reason) 을 반환하고 파일을 덮어쓰거나 재생성하지 않는다. store 는 write-through — 변경(템플릿 생성 · 갱신)은 그 자리에서 즉시 영속되고 지연 쓰기 · dispose flush 가 없다; 따라서 닫기 절차(ED-16) 중 store 쓰기는 0 이고 사용자 저장을 덮어쓰는 순서가 생기지 않는다(R2-05).
ProjectDirectoryDetector(workspace) — pubspec.yaml 탐색(무시 규칙 적용) → yaml 파싱 → 판정 3종(D-015). 캐시 무효화: §4.2 ⓑ.
- 앱
RunConfigService(워크스페이스 스코프, §4.9): 설치 직후 템플릿 생성(PRD §2.5 정본, 프로젝트 디렉터리마다 kind 별 1건) · 전제 검사(ProcessLauncher.run 으로 which/where · serverpod --version · flutter devices --machine, 프로파일 ED-12) · 실행 = StartRunConfigurationUseCase → ProcessLauncher.start(ProcessLaunchRequest(executable, args, workingDirectory, environmentOverrides, profile: humanTerminal)) → LaunchedProcess → TerminalSessionManager.attach(origin: runConfiguration(id))(백엔드 = 파이프 LauncherTerminalBackend — 실행 구성은 대화형 입력이 필요 없어 PTY 가 아니어도 AC-08 충족; PTY 승격은 EPIC-6 뒤 선택 [ED-08]).
- 중지:
StopSessionUseCase → LaunchedProcess.terminate({Duration grace}) — core 계약에 메서드 추가(SIGTERM → grace 대기 → SIGKILL, 런처 구현 소유; 메서드 추가라 contractTypes 불변, AR-03). 현행 bool kill()(sandbox_types.dart:192)은 그대로 둔다. grace 값은 §10.
4.6 PTY 터미널 (FR-6xx) — [ED-06] 기동 경로는 런처 메서드 하나, spawn 구현은 의존 역전으로 주입
문제: C-05/DD-15 는 "프로세스 시작은 런처뿐"인데 PTY(Pty.start, FFI forkpty)는 Process.start 가 아니고, flutter_pty2 는 Flutter 플러그인이라 순수 Dart 인 toolchain(3-OS dart test)에 넣을 수 없다. rev.1 의 "티켓 + 별도 spawn 파일" 안은 spawn 사이트가 둘이 되고 티켓을 손수 만들어 정책을 우회할 수 있었다(AR-01 · AR-02).
결정:
core 협력 타입 파일 pty_types.dart(계약 목록 밖, contracts.dart 머리말 규칙): PtyLaunchRequest{ProcessLaunchRequest base, int rows, int cols, bool loginShell} — ProcessLaunchRequest 는 final class(sandbox_types.dart:66)라 상속이 아니라 합성이며 base.isTerminal == true 를 생성자가 단언한다(AR-19 · R2-06b) · PtyHandle{output, write, resize, exitCode, kill, terminate({Duration grace})}(R2-04 — 마스터 닫기/SIGHUP → grace 대기 → SIGKILL) · abstract interface class PtySpawner { PtyHandle spawnPty(ResolvedPtyLaunch launch); }(P-02). ResolvedPtyLaunch 는 공개 타입이다 — Dart 가시성은 라이브러리 단위라 패키지 간 비공개 생성자는 성립하지 않는다(R2-06a); 손수 생성 우회는 4번의 스캔 2종이 막는다.
core.ProcessLauncher 에 Future<PtyHandle> startPty(PtyLaunchRequest request) 추가(메서드 추가 → contractTypes 불변). ToolchainProcessLauncher 는 생성자 인자 PtySpawner? ptySpawner 를 받아, startPty 에서 정책(프로파일 · DD-23a resolveEnvironment · cwd 검증 · 로그인 셸 인자) 을 적용해 ResolvedPtyLaunch 를 만든 뒤 주입된 spawner 를 호출한다. spawner 가 없으면 PtyUnavailable 결과(AC-23 표시 대상).
terminal FlutterPtySpawner implements PtySpawner — Pty.start(...) 텍스트가 있는 유일한 파일. 부트스트랩(§4.9)이 ToolchainProcessLauncher(ptySpawner: FlutterPtySpawner()) 로 주입한다. terminal 은 core 만 의존(L1 의존 없음, AR-15).
- 가드:
check_process_launch_allowlist.py 의 _PROCESS_LAUNCH 에 Pty\s*\.\s*start\s*\( 를 추가하고 ALLOWLIST 에 package/terminal/lib/src/backend/pty/flutter_pty_spawner.dart 를 등재. 추가 스캔: PtySpawner 구현 타입의 생성 지점이 앱 부트스트랩 1곳뿐임과 .spawnPty( 호출처가 process_launcher.dart 뿐임을 확인(AR-02 · P-02: Isolate.spawn( 과 텍스트 충돌 없음). 회귀 테스트에 두 케이스 추가.
- 로그인 셸: macOS/Linux
$SHELL -l(없으면 /bin/bash -l). [ED-11] Windows 셸 선택 순서 = pwsh → powershell → %COMSPEC%(cmd) — 근거: 로그인 프로필이 PATH 를 복원하는 순서(OBJ-1-Architect-6). PRD U-13 은 이 결정으로 닫힌다. 환경은 DD-23a 통과분 — TERM=xterm-256color · COLORTERM=truecolor 는 EnvironmentWhitelist.forPlatform(terminal: true) 목록에 있어야 resolveEnvironment 를 통과한다(process_launcher.dart:99-128 은 목록 밖 키를 EnvironmentNotAllowed 로 거부) — 목록에 없으면 EPIC-6 S3b 가 화이트리스트를 갱신한다(AR-18).
PtyTerminalBackend implements CocodeTerminalBackend — PtyHandle 을 감싼다: output = pty 출력(UTF-8 디코더, 불완전 시퀀스 보존), write = pty 입력(사용자 키 · IME 확정 문자열만 — 불변식 (1)), resize = pty 창 크기. CocodeTerminalCapabilities(isPseudoTerminal: true, canResize: true).
- scrollback 상한:
CocodeTerminalBuffer.maxLines 는 이미 존재하고 기본 상수 kCocodeDefaultScrollbackLines = 5000(cocode_terminal_buffer.dart:25)이 있다(AR-08) — 재사용하며 값을 설정 항목으로 노출한다. 5000 의 근거 기록은 §10 U-2.
flutter_pty2 스파이크(EPIC-6 S1)가 실패하면 자체 FFI(forkpty · Windows ConPTY)로 PtySpawner 를 구현 — 인터페이스 동일.
4.7 언어 지원 (FR-4xx)
app/feature/language LanguageServerService(워크스페이스 스코프): start(root) →
ProcessLauncher.start(ProcessLaunchRequest('dart', ['language-server', '--client-id=coco'], cwd: root, profile: ED-12))
→ ProcessLspTransport(process) → CocodeLspClient(transport, documents: EditorDocumentSource, sink: CocodeSemanticTokenSink, diagnostics: CocodeDiagnosticsSink)
core contracts.dart: abstract interface class DiagnosticsSink { void publish(Uri uri, List<Diagnostic> items); void serverStatus(LanguageServerStatus s); } ← 13번째 계약 [ED-09]
diagnostics_types.dart: Diagnostic(range, severity, message, code?) · LanguageServerStatus{starting, running, absent, exited(code)} (협력 타입, AR-12)
editor CocodeDiagnosticsSink(ChangeNotifier) → 거터 마커 + ProblemsBloc 요약(개수 · 목록 스냅샷)
lsp CocodeLspClient({..., DiagnosticsSink diagnostics = const NullDiagnosticsSink()}) — publishDiagnostics → sink.publish
- [ED-09]
DiagnosticsSink 는 core 계약(13번째, contract_registry.dart:12 갱신 + DD-02a 완결성 테스트 갱신). toolchain 은 이 타입을 import 하지 않는다(C5) — check_package_layers.py 의 L1→L2 검사로 충분.
- 서버 상태는
sink.serverStatus → [문제] 탭 상단 배너. 종료 시 자동 재기동은 하지 않는다(첫 릴리스 — 사용자가 [문제] 탭에서 "다시 시작") [ED-10].
- 비-Dart 강조:
editor LanguageIdResolver(파일 이름 → Dockerfile, 확장자 → sql/yaml/markdown) + re_highlight 언어 등록. CocodeEditorView.languages 에 매핑.
4.8 사람 편집 × 작업 (FR-7xx) 와 상위 Story
app/feature/crossing TaskFileIndicator: 열린 문서의 pathKey 로 SnapshotStore(상위 계약)를 조회해 실행 중(실행중 · 자가검증중) 작업의 스냅샷 존재 여부 → CocodeTaskFileBanner(taskId). 저빈도(문서 열림 · 작업 상태 변경 시 갱신).
- D-013(상위 Story):
RollbackTargetSet.structuralConflicts(core/lib/src/domain/rollback_targets.dart:459-480)에 조건 추가 — 대상 작업의 경로별 스냅샷 시퀀스(SnapshotChainStore.snapshotsOfTask, snapshot_chain_store.dart:84)에서 next.baseContentHash != prev.resultContentHash 이면 RollbackConflict.chainDiscontinuity(path, prevSnapshotId, nextSnapshotId) 를 보고. RollbackConflict 는 sealed 5변형(rollback_targets.dart:260-428)이라 exhaustive switch 전수 갱신(ui cocode_rollback_conflict.dart 매핑 포함)이 상위 Story 인수조건에 들어간다(AR-16). 충돌을 늘리는 방향이라 RollbackConflictCheck 문서 주석("관대해지지 않는다")과 정합.
4.9 셸 조립 · 워크스페이스 스코프 컨테이너 (FR-1xx) — [ED-13]
app/cocode/lib/main.dart _pane: navigation → NavigationPane(탭: 탐색 · 위임(상위) · 검색) · editorStack → EditorStackPage · terminal → BottomPane(탭: 터미널 · 문제 · 검증 로그(상위)) · inspector → 상위 CocodeTaskInspector · workbench → 컨테이너.
- 부트스트랩(앱 수명):
registerContracts() — 계약 13종 정확히 1회(contractTypes 대조 테스트, DD-02a 두 축). ToolchainProcessLauncher(ptySpawner: FlutterPtySpawner()) 를 ProcessLauncher 로 등록. DI 수단은 CLAUDE.md 대로 GetIt 수동 등록(계약 13종 + 앱 서비스), Bloc 은 BlocSignalProvider(상위 §2.3).
- 워크스페이스 스코프 컨테이너(열기~닫기 수명) — 생성 순서와 dispose 역순(AR-05):
| 순서 |
서비스 |
생성 |
의존 |
dispose 시 |
| 1 |
DocumentBufferRegistry |
워크스페이스 열기 |
— |
마지막 — dirty 처분 완료 후 release 전수 |
| 2 |
IgnoreMatcher · ProjectDirectoryDetector(캐시) |
1 직후 |
ProcessLauncher |
캐시 폐기 |
| 3 |
FileChangeWatcher |
2 직후 |
1 · 2 |
구독 해제 |
| 4 |
TerminalSessionManager |
3 직후 |
ProcessLauncher |
세션 전수 terminate |
| 5 |
RunConfigService |
4 직후 |
2 · 4 · RunConfigurationStore |
없음 — store 는 write-through(ED-17), 닫기 중 쓰기 0 |
| 6 |
LanguageServerService |
5 직후(지연 기동 가능) |
ProcessLauncher · 1 |
첫 번째 — 서버 종료 |
dispose 순서: 6 → 5 → 4 → 3 → 2 → 1. 이 6종은 계약이 아니므로 DD-02a 완결성 테스트 대상이 아니다 — 대신 app/cocode/test/src/feature/shell/workspace_scope_test.dart 가 생성 · dispose 순서를 단언한다.
- [ED-16] 닫기 · 종료 절차와 고아 프로세스 0 보장(E-C1 · E-I29).
workspace.close/app.quit 은 ① CocodeDirtyDispositionDialog(일괄 변형 — dirty 목록 + 실행 중 세션 목록)에서 사용자가 "계속" 을 고른 경우에만 진행한다(취소 가능한 유일한 지점) ② 검색 취소 토큰 발화 ③ LanguageServerService.stop()(앱 소유 프로세스, 목록에 없음) ④ TerminalSessionManager.terminateAll(grace) — 세션 종류별 terminate({grace})(파이프 세션 = LaunchedProcess.terminate, PTY 세션 = PtyHandle.terminate, R2-04) 후 exited 확인, 미종료가 남으면 SIGKILL 까지 완료해야 다음 단계로 ⑤ 감시 해제 ⑥ 버퍼 해제. 일괄 저장 중 한 파일이라도 HumanWriteFailed 면 ①에서 진행을 멈추고 실패 목록을 다이얼로그에 남긴다(E-C2, UX-E-19). 앱 창 닫기 이벤트는 ①로 라우팅된다(강제 종료 경로 없음). 고아 프로세스 0 은 정상 종료 경로에 한한다(P-01) — 앱 크래시 시 파이프 자식은 남을 수 있고 PTY 자식만 마스터 닫힘(SIGHUP)으로 정리된다.
- 메뉴 · 팔레트:
CocodeCommandRegistry 에 editor.save · editor.saveAll · editor.closeTab · search.workspace · search.replaceAll · run.start(id) · run.stop(session) · terminal.new · terminal.kill · language.restart 등록; 메뉴바는 CoUI Menubar flat 항목. 키 조합 표는 미정(UX-D-16 · U-11).
5. 데이터 아키텍처
| 데이터 |
형식 · 위치 |
소유 |
쓰기 허용(DD-15) |
| 문서 버퍼 |
메모리 |
editor 레지스트리 |
— |
| 사람 편집 저장 |
워크스페이스 파일(.cocode/** 거부, .cocode/editor/** 허용) |
workspace/lib/src/human_edit/ |
신설 행 |
| 실행 구성 |
.cocode/editor/run_configurations.json — {version, items:[{id,name,templateKind,executable,arguments[],workingDir,env{}}]} |
workspace/lib/src/run_config/ |
신설 행 |
| 터미널 세션 · scrollback |
메모리(CocodeTerminalBuffer.maxLines) |
앱 TerminalSessionManager · terminal |
— |
| 무시 판정 · 프로젝트 디렉터리 판정 캐시 |
메모리(§4.2 ⓑ 로 무효화) |
workspace |
— |
| 진단 |
메모리 |
editor sink |
— |
| 체인 불연속 |
파생(스냅샷 두 개의 관계) |
상위 .cocode/history |
— |
6. NFR 커버리지
| NFR |
해법 |
검증 |
| NFR-01 응답성 |
DD-06 + ED-02 · 트리 지연 적재 · 검색 isolate · 비활성 탭 뷰 언마운트(컨트롤러 유지) |
ac15_responsiveness_bench.dart 를 탭 N(U-1) 조건으로 |
| NFR-02 IME |
새 표면 5종에 I1~I3 · probe.ac16-ime-replay.yaml 에 검색 · 팔레트 · 실행 구성 편집 표면 추가(benchmark/ac16_ime_replay.dart 대상 확장) |
상위 cocode/AC-16 판정(분리) |
| NFR-03 자원 |
maxLines · 감시는 열린 문서 부모 + 메타 파일 부모(.gitignore · pubspec.yaml · .cocode/editor/run_configurations.json) · 검색 취소 토큰 · 종료 시 프로세스 전수 terminate(ED-16) |
단위 테스트(규칙) |
| NFR-04 보안 |
ED-05 경로 · realpath · .cocode/ 거부 · ED-06 단일 런처 메서드 + 주입 · DD-23a · 허용 목록 가드 3종 |
가드 스크립트 CI |
| NFR-05 접근성 |
K1 등재 테스트(CocodePaletteCoverage) · K4 · 44dp |
위젯 테스트 |
| NFR-06 유지보수 |
신설 ui 위젯 4종 위젯북 · should_when · 래칫 무변화 · 유즈케이스 단위 테스트 |
CI |
7. 보안 · 집행 지점 (상위 §5.2 확장)
| 대상 |
집행 |
어디서 |
| 사람 편집 저장 |
realpath 포함 검증 · .cocode/** 거부(.cocode/editor/** 예외) · 스냅샷/리스 계약 미호출 |
HumanEditWriter + AC-13 스캔 |
| 실행 구성 · 언어 서버 · git · 버전 조회 |
단일 런처 run/start, 프로파일 ED-12 |
ToolchainProcessLauncher |
| PTY 셸 |
단일 런처 startPty(정책) → 주입된 spawner(구현 1파일) |
ED-06 · 가드 확장 |
| 앱 명령의 stdin 타이핑 |
부재 — PtyTerminalBackend.write 호출처는 뷰의 키 입력 핸들러뿐 |
AC-12 스캔 테스트 |
| 무음 실패 |
기동 실패 · 종료 코드 · 저장 실패 · PtyUnavailable 을 세션 자리 Alert / 본문 상단 Banner / 문제 탭으로(토스트 없음, UX-E-37) |
AC-23 실패 주입 테스트 |
[ED-12] 앱 발 도구 프로세스의 프로파일. 언어 서버 · git check-ignore · 버전 조회 · 실행 구성은 상위 §5.2 표의 두 범주(에이전트 명령 / 사람이 여는 터미널) 어디에도 정확히 속하지 않는 제3 범주(사용자 트리거, 앱이 인자를 구성)다(AR-17). 이 모듈은 humanTerminal 프로파일을 쓴다 — 근거: 경로 · 호스트 집행은 AC-14 의 Given(에이전트 작업)에 종속되어 불필요하고, DD-23a 환경 화이트리스트는 프로파일과 무관하게 적용된다. 상위 SandboxProfileRegistry 에 새 프로파일을 추가하지 않는다.
8. 테스트 전략
| 층 |
무엇 |
어디서 실행 |
| 순수 Dart 단위 |
core 엔티티 · 유즈케이스 6종 · workspace writer(거부 목록 · realpath)/scanner/detector/store · toolchain startPty 정책 · terminate |
verify.cocode-pure-tests.yaml 3-OS dart test |
| Flutter 위젯 |
editor source/sink · terminal spawner(가짜 pty)/백엔드 · ui 신설 위젯 4종 · 앱 셸(cocoTestApp) · 스코프 컨테이너 순서 |
ci.yml test 잡 — app/cocode/lib 편입(FR-102) |
| BDD → Patrol |
docs/bdd-fullstack-code-editor.md 83건(v1.1) → app/cocode/integration_test/features/*.feature + 손으로 쓴 step · 배치 시나리오(Scaffold 단계) |
Patrol nightly(데스크톱 러너 — 상위 docs/ac16-macos-spike-cocode.md 러너 전제) |
| 가드 |
허용 목록 3종(+Pty.start · spawner 생성 지점) · 계층 래칫 · DD-02a 완결성 · K1 커버리지 · AC-11/12/13 스캔 |
ci.yml changes 잡 |
| 실측 프로브 |
PTY 3-OS 스파이크 · IME 재생 · ac15 탭 N |
probe 워크플로 |
9. 이 설계가 내린 결정 (ED 목록)
| ID |
결정 |
근거 |
| ED-01 |
신설 패키지 0 — 기존 7 패키지 확장 + 앱 |
DD-00 K 기준 · 배치표 §1 |
| ED-02 |
앱 레벨 레지스트리 · 매니저는 알림형, Bloc 은 저빈도 요약만 — 필드 열거 테스트로 고정 |
DD-06 · OBJ-1-Architect-4 |
| ED-03 |
유즈케이스 6종(core/usecase/editor/) + Bloc optional constructor injection · 계약 명명은 상위 관례(I 접두어 없음, §11) |
CLAUDE.md · 상위 §2 · AR-04 |
| ED-04 |
버퍼 수명 = DocumentBufferRegistry 소유, 탭은 참조 |
Seed 불변식 (2) · AC-03 |
| ED-05 |
사람 편집 저장은 게이트와 다른 파일(human_edit/) — realpath · .cocode/** 거부 · sealed 결과 · 쓰기 허용 행 신설 |
C-03 · AC-13 · DD-15 · AR-10 · AR-13 |
| ED-06 |
PTY 기동 = ProcessLauncher.startPty 단일 메서드(정책) + core.PtySpawner 협력 인터페이스를 부트스트랩이 주입(구현 terminal 1파일) · 가드에 Pty.start + spawner 생성 지점 스캔 · toolchain 순수 Dart 유지 |
C-05 · DD-15 · DD-23a · AR-01 · AR-02 |
| ED-06a |
git 저장소: 판정 정본 = git check-ignore + 캐시 · 감시 무효화 / 비-git: 루트 .gitignore 만 Dart matcher / 파일 없음: 제외 0 |
FR-302 · Seed AC-01 |
| ED-07 |
실행 구성 영속 = .cocode/editor/run_configurations.json(신설 허용 디렉터리 editor/), .cocode/config/ 는 #22 소유라 불가침 |
cocode_layout.dart:49,53 · AR-11 |
| ED-08 |
실행 구성 세션은 파이프 백엔드로도 AC-08 충족 — PTY 승격은 선택 |
AC-08 문면 |
| ED-09 |
DiagnosticsSink = core 13번째 계약, 구현은 editor, lsp 는 선택 인자로 전달(기본 NullDiagnosticsSink), toolchain 미의존 |
C1 · C5 · #60 AC3 · AR-14 |
| ED-10 |
언어 서버 자동 재기동 없음(수동 "다시 시작") |
YAGNI · AC-07 |
| ED-11 |
Windows 명령 셸 순서 pwsh → powershell → %COMSPEC% — PRD U-13 닫힘 |
OBJ-1-Architect-6 · AR-07 |
| ED-12 |
앱 발 도구 프로세스(언어 서버 · git · 버전 조회 · 실행 구성)는 humanTerminal 프로파일 — 제3 범주임을 명시 |
상위 §5.2 · AR-17 |
| ED-13 |
워크스페이스 스코프 컨테이너 6종의 생성 · dispose 순서 표 · GetIt 수동 등록 · 계약 밖 서비스는 순서 테스트로 |
CLAUDE.md DI · 상위 §2.3 · AR-05 |
| ED-14 |
파일 감시 = 문서 부모 디렉터리 + 메타 파일(.gitignore · pubspec.yaml · .cocode/editor/run_configurations.json) 두 범위, 자체 저장은 해시 대조로 억제 |
AR-09 · flutter_ide §6-b4 |
| ED-15 |
LaunchedProcess.terminate({grace}) 계약 메서드 + PtyHandle.terminate({grace}) 추가(SIGTERM/SIGHUP → 대기 → SIGKILL) — kill() 은 유지. 기존 구현체(_IoLaunchedProcess + terminal · acp · agent 테스트 fake) 전수 갱신이 Story 인수조건(P-06) |
AR-03 · R2-04 |
| ED-16 |
닫기 · 종료 절차 6단계 고정 — 다이얼로그(dirty + 실행 중 세션)에서만 취소, 언어 서버 무조건 종료, 세션 전수 terminate 후 진행, 일괄 저장 부분 실패 시 진행 중단 — 고아 프로세스 0 |
E-C1 · E-C2 · E-I29 |
| ED-17 |
RunConfigurationStore 두 쓰기 주체 규칙 — 프로그램 쓰기는 store 만(write-through, dispose flush 없음, 닫기 중 쓰기 0), 파일 감시로 재읽기, dirty 편집 중이면 외부 변경 흐름, 파싱 실패 시 재생성 없음 |
UR-11 · E-I25 · R2-05 |
| ED-18 |
L0/L2 의 경로 키 · 해시는 String(상위 EditSnapshot 관례) — PathKey/ContentHash 변환은 workspace 경계, core.contentHashOf ↔ workspace.ContentHash parity 테스트 |
R2-02 · 배치표 §1 |
| ED-19 |
HumanEditStore(read · write) · WorkspaceSearchPort 는 계약이 아니라 core/usecase/editor/ports.dart 의 협력 타입(포트) — 계약 수 13 유지, 구현(HumanEditWriter · WorkspaceSearch)은 ED-13 컨테이너가 유즈케이스에 주입 |
R2-03 · DD-02a |
10. 값이 없어 이 문서가 정하지 않은 것
- dirty 해시 디바운스 간격(P-08) — 값 출처 없음, EPIC-2 벤치(ac15 절차)로 정한다.
| # |
항목 |
확정 주체 |
| 1 |
감시 감지 지연 상한(U-3) · terminate grace 기본값 · 탭 N(U-1) |
Design 후속 Story(설정 기본값) · 상위 D-065 절차 |
| 2 |
scrollback 기본값(U-2) — 코드에 kCocodeDefaultScrollbackLines = 5000 이 이미 있으나 근거 기록이 없다. U-2 는 "값 결정"이 아니라 "5000 의 근거를 기록하거나 설정 기본값으로 재확정" 으로 좁힌다 |
EPIC-6 S5 |
| 3 |
DiagnosticsSink 값 타입 상세(LSP Diagnostic 필드 부분집합) |
~~EPIC-4 S3~~ → 확정(#343 · D-042): range · severity · message · code? 넷. 고른 기준은 「화면이 그리는 것」 |
| 4 |
run_configurations.json 스키마 버전 정책 |
EPIC-5 S1 |
| 5 |
PTY 패키지(U-8) |
EPIC-6 S1 스파이크 |
| 6 |
메뉴 항목 표 · 키 조합(U-11) |
ux-spec + Breakdown 이슈 |
| 7 |
바이너리 정책(U-12) · .gitignore matcher 선택 · 캐시 무효화 상세(U-14) |
EPIC-2 · EPIC-3 후속 |
| 8 |
D-013 · D-018 |
상위 파이프라인 |
| 9 |
TERM/COLORTERM 의 화이트리스트 등재 여부(AR-18) — 실측 후 갱신 |
EPIC-6 S3b |
11. Seed · PRD · 상위 문서와의 어긋남 / 공백 (숨기지 않고 적는다)
| # |
어긋남 |
처리 |
| 1 |
Seed §0 "단일 런처 = 프로세스를 시작할 수 있는 유일한 경로" vs Pty.start 텍스트가 있는 파일이 process_launcher.dart 밖(terminal spawner)에 1개 존재 |
기동 경로(호출 지점)는 ProcessLauncher.startPty 하나이고 spawner 는 그 메서드만 호출한다(ED-06). 가드 허용 목록에 2번째 파일 등재 — 문면상 "파일 하나"에서 "메서드 하나 + 주입 구현 1파일"로 해석을 넓힌 것이며 Seed 의무(앱 명령을 stdin 에 쓰지 않음 · 정책은 런처가 집행)는 유지된다 |
| 2 |
CLAUDE.md I 접두어 vs 상위 계약 명명(접두어 없음) |
상위 관례를 따른다(§3) |
| 3 |
상위 §5.2 의 두 범주에 없는 앱 발 도구 프로세스 |
ED-12 로 결정 · 상위에 통보 |
| 4 |
PRD U-13(Windows 셸) |
ED-11 로 닫힘 — PRD §10 갱신 |
| 5 |
rev.1 §11 이 "없음"이라 적은 것은 잘못 — rev.2 에서 정정 |
AR-21 |
| 6 |
상위 CocodeStatusBar 의 4칸 고정 조립 — 이 모듈이 칸을 더하려면 상위 위젯 additive 변경이 필요(UR-02) |
§2 ui 행에 등재, 상위 4칸 계약은 불변 |
| 7 |
rev.3 이 L1 타입 PathKey/ContentHash 를 L0/L2 에서 참조했다(계층 위반) |
rev.4: String + parity 테스트(ED-18) |
| 8 |
rev.3 이 HumanEditStore · WorkspaceSearchPort 를 "계약"이라 부르면서 계약 수를 13 으로 적었다 |
rev.4: 포트(협력 타입)로 분류(ED-19) |
| 9 |
rev.3 ED-06 1번이 구현 불가한 문장 2개(패키지 간 비공개 생성자 · final class 상속)를 담았다 |
rev.4: 공개 타입 + 스캔, 합성(R2-06) |
Generated by cc-product Design stage (Architect) · 2026-09-16 · rev.4