Docs · Cocode ADE · 분석 · 설계 문서

Cocode ADE · 11

Architecture — 아키텍처 설계

패키지 구조 · 레이어 · 에이전트 런타임 계약 · EditSnapshot 저장 · 샌드박스 집행

목차

파이프라인 4단계(Design) 산출물 · 2026-08-26 · 아키텍처 설계 정본 순서: Seed Spec v3.1.0 > 조직 규약(cc-flutter·cc-coui 스킬) > PRD/BDD > Discovery

작성 방식: 초안을 작성한 에이전트와 별도의 검증 에이전트가 원본 파일을 직접 열어 대조하고 교정했다. 이 파일은 교정본이다.


⚠️ 이 문서 작성 이후 확정된 사항 (2026-08-26, 반영 필요)

아래 셋은 이 문서를 생성한 워크플로가 시작된 뒤 확정됐다. 본문에 반영돼 있지 않으므로 이 블록이 우선한다.

1. Flutter master 채널 + 네이티브 윈도잉 API (D-022) — 창 분리를 window_manager 가 아니라 Flutter 네이티브 윈도잉(RegularWindow·DialogWindow·PopupWindow·SatelliteWindow·WindowingOwner·WindowRegistry)으로 구현한다. 결정적 근거는 windowHandle 이 ffi.Pointer<ffi.Void> 로 네이티브 핸들(Linux GtkWindow·macOS NSWindow·Win32 HWND)을 노출해 네이티브 도킹이 가능하다는 점이다(_window_linux.dart:208, _window_macos.dart:200).

  • 채널 제약: windowingFeature 가 master: 만 선언하므로 stable 에서는 available:false 이고 설정으로도 켤 수 없다(flutter_features.dart:99 가 config 를 읽기 전에 반환). 제품 빌드 전체가 master 채널 위에 서게 된다.
  • 인수한 위험: @internal API · 패치 버전에서도 breaking change 예고 · CI·개발 환경·cob doctor 가 모두 master 전제. 완화로 창 관리 호출을 얇은 자체 인터페이스 뒤에 두어 수정 지점을 한 곳으로 모은다.

🔄 D-022 재분류 — MVP 범위 밖 (#100 / D-025, 2026-08-27 · 반영 #54)

프로젝트 오너가 Flutter 3.47.x stable 유지를 확정해 D-022 의 채널 전환이 취소됐다. stable 에서는 다중 창 기능 플래그가 master: 만 선언해 설정으로도 켤 수 없으므로 (flutter_features.dart 가 config 를 읽기 전에 반환), D-022 가 전제한 다중 창 · 네이티브 핸들 도킹은 MVP 에서 합격 판정 자체가 성립하지 않는다.

따라서 위 항목 1 은 취소가 아니라 재분류다 — 결정의 기술적 내용은 유효하되 MVP 범위 밖이며, 그 자리에 남는 것은 완화책 한 줄(「창 관리 호출을 얇은 자체 인터페이스 뒤에 두어 수정 지점을 한 곳으로 모은다」)뿐이다. 그 인터페이스는 #54 가 package/desktop_platform 에 만들었고, 구현은 단일 창이며 분리·도킹은 명시적 미지원을 돌려준다(무언 실패 금지). 미래의 네이티브 구현은 그 seam 에 꽂는다.

집행: .github/scripts/check_flutter_stable_pin.py(채널·SDK 심볼 전역 0건) + .github/scripts/check_window_api_seal.py(창을 다루는 수단이 desktop_platform 밖에서 0건).

이 재분류가 걸리는 본문 지점: §0 오너 결정 5 · §1.3 desktop_platform 행 · §1.4 desktop_platform 문단 · §2.4 창 분리·도킹 크롬 행 · §9 미결 15. 각 지점은 이 블록을 가리키며, 그 아래 서술은 「master 채널이 채택됐다면」의 조건부로 읽는다.

2. 프로젝트 초기 세팅은 cob 로 수행 (D-023) — 모노레포 생성과 기능 추가를 cob(co-bricks CLI)로 한다. cob doctor 전 항목 통과 확인. 관련 명령: cob create·cob generate(project.yaml → 모노레포+기능)·cob compose/add·cob apply·cob plan(PRD FR/AC 커버리지 게이팅, 고아 FR hard-fail). 미결: cocode ADE 는 앱이 아니라 데스크톱 IDE 이고 Serverpod 백엔드가 없어, 기존 brick 이 이 형태를 지원하는지 Scaffold 단계에서 cob list-features 로 확인해야 한다.

3. CoUI 실측 인벤토리 (docs/coui-inventory-cocode.md) — 이 문서가 인용하는 CoUI 컴포넌트는 그 인벤토리와 대조해야 한다. 특히 주의: coui-dock 은 IDE 도킹이 아니라 모바일 하단 내비게이션 바이고, coui-diff 는 코드 diff 가 아니라 이미지 비교 슬라이더다(코드 diff 는 coui-code-diff). Figma 검색 결과 Terminal·Toolbar·StatusBar·Sidebar·Panel(단독)·Input·Split·CommandPalette 는 실재하지 않는다.


Architecture: 코코드 ADE (cocode ADE)

파이프라인 4단계(Design) 산출물 · 2026-08-26 (rev.2 — 리뷰 지적 반영) 정본 순서: Seed Spec v3.1.0 > 오너 확정 결정(의사결정 일지) > 조직 규약(cc-flutter·cc-coui 스킬) > PRD/BDD > Discovery. Seed Spec 과 어긋나는 서술이 있으면 그것은 결함이며, 이 문서는 어긋나는 지점을 §10에 모아 명시한다. 이 문서가 새로 내리는 결정은 DD-NN으로 식별한다 — 의사결정 일지의 D-001~D-023, PRD §4의 D-1~D-22와 번호 체계가 다르다(세 체계가 이미 충돌 중이므로 접두어를 분리했다).


0. 이 문서가 서 있는 지반

항목 상태 근거
Seed Spec v3.1.0 · DRAFT(Specification Gate 미통과). 최근 측정치 모호성 0.3125 는 v3.0.2 기준이며 v3.1.0 은 재평가 대기다 seed-spec 8행, prd 8행
이 문서의 지위 Seed Spec 을 구현 가능하게 만드는 층. 경계를 넓히거나 좁히지 않는다 prd 10행("PRD 는 Seed Spec 을 부연하며, 뒤집지 않는다")과 같은 원칙
이 문서가 해소해야 할 위임 항목 PRD §4 D-2(①~④ 적용 불가 판정), D-3(응답 불가 감지), D-12(검증 판정 파라미터), D-17(edit_locks 초기값), D-19/D-20/D-22(샌드박스), CH-022(EditSnapshot 보존 정책) prd 629·630·639·644·646·647·649행, contrarian 144·174행

오너 확정 결정 6건을 전제로 삼는다. 이 여섯은 뒤집지 않는다.

# 결정 Seed Spec·근거와의 관계
1 agent 를 ACP 서버로 노출하지 않는다. 내부는 Dart 계약 인터페이스 직결, acp 가 외부 ACP 에이전트를 그 인터페이스로 어댑터한다 충돌 없음. AC-06 은 "자체 런타임과 외부 ACP 호스팅 두 경로 제공"만 요구하고 자체 런타임의 ACP 노출을 요구하지 않는다(seed-spec 108행). 노출을 제안한 것은 Discovery §Ideation 도해(discovery 98행)이며 PRD 77행이 이미 "채택 결정 기록이 없다 → Design 결정"으로 넘겼다
2 상태 관리는 BlocSignal 조직 표준. 정본은 ~/.claude/skills/cc-flutter/references/patterns/bloc-patterns.md(bloc_signals·bloc_signals_flutter·bloc_signals_test·bloc_signals_lint — 같은 문서 17~20행). ⚠️ cc-flutter/skills/flutter-patterns/SKILL.md 본문은 아직 BLoC(26·42·44·46·47·128·132행)·BlocProvider(51행) 표기 — 같은 스킬 팩 안에서 표기가 갈린다
3 EditSnapshot 은 파일 기반 저장(Lumide local_history 방식) D-003 Rationale 이 "Lumide 의 local_history 설계와 같은 축"이라 기록(decision-log 37행). ⚠️ 같은 줄이 적은 정량값(50버전/5MB)은 PRD R-4(718행)가 재인용을 금지하므로 이 문서는 방식만 승계하고 값은 옮기지 않는다. 보존 정책은 CH-022 가 Design 으로 미룬 항목
4 UI 는 CoUI 기반 + IDE 전용 확장 Discovery 를 뒤집는다. Discovery §Technical Feasibility 는 UI 로 material_ui/cupertino_ui 1.0 을 권장한다(discovery 117행). 조직 규약(cc-flutter coui-flutter-rules, cc-coui coui-composition-and-extension)이 우선하며, Discovery 의 그 행은 채택하지 않는다
5 🔄 MVP 밖으로 재분류됨(#100/D-025 — 상단 「D-022 재분류」 블록). ~~Flutter master 채널 + 네이티브 윈도잉 API~~ · 남는 것은 완화책(얇은 자체 인터페이스)뿐이고 그것이 desktop_platform 이다(#54) decision-log 256행 D-022(오너 확답, 두 차례 확인). Impact 가 "아키텍처 문서의 플랫폼 계층"을 명시했고, 완화책으로 "창 관리 호출을 얇은 자체 인터페이스 뒤에 두어 수정 지점을 한 곳으로 모은다"를 확정했다. 인수한 위험: master 채널이 제품 빌드 기반 전체에 적용됨 · @internal API 이며 패치 버전에서도 breaking change 예고 · A-03 스파이크도 master 에서 측정해야 함
6 모노레포 초기 생성·기능 추가는 cob 로 수행 decision-log 281행 D-023(오너 지시). Impact 가 "아키텍처 문서의 프로젝트 부트스트랩 절"을 명시. 미결: cocode ADE 는 앱이 아니라 데스크톱 IDE 이고 Serverpod 백엔드가 없어, 기존 brick 이 이 형태를 지원하는지는 Scaffold 단계에서 cob list-features 로 확인해야 한다

rev.2 에서 고친 것(요약). ①D-022·D-023 누락 복구 및 desktop_platform 신설 ②K 기준을 자기 패키지 목록이 통과하도록 재정의(K4 신설) ③AgentSession 에 요청-응답 채널 추가 ④롤백 사전검사 비교 대상 정정 ⑤보존 정책의 값 논증에서 유도 수치 제거, P3/P4 우선순위 명시 ⑥verification_log 폐쇄 코드 주장 범위 축소 ⑦환경변수 화이트리스트에 HOME 계열 복구 ⑧path_key 정규화 규칙 ⑨③의 대상없음 조건을 판정 없는 형태로 바꿔 ⑤의 사문화 해소 ⑩⑤ 자체의 실행 정의와 파서 레지스트리 추가 ⑪DD-17 의 "확정" 주장을 철회하고 fail-closed 로 교체 ⑫AC-18 리스 논증 정정.


1. (a) 패키지 구조

1.0 부트스트랩 (오너 결정 6)

모노레포 초기 생성은 cob generate/cob create-from-config, 이후 기능 골격 추가는 cob compose/cob add/cob apply 로 수행한다(D-023). 도구 체인 점검은 cob doctor·cob preflight 이며, cob doctor 는 Flutter 채널이 master 인지도 점검해야 한다(D-022 Impact). CI 워크플로의 Flutter 설치 단계도 master 로 고정한다.

cob 는 cocode ADE 자신을 만드는 도구이고, §1.3 의 bricks 는 cocode ADE 사용자의 워크스페이스를 만드는 런타임 컴포넌트다 — 둘은 다른 층위이며 서로를 의존하지 않는다.

1.1 먼저 정정 — "Discovery 가 제안한 9분할"은 존재하지 않는다

작업 지시는 Discovery §Ideation 이 core/editor/terminal/lsp/agent/acp/bricks/ui/cocode_app 9분할을 제안했다고 적었으나, docs/ 전체 grep 결과 core·editor·terminal·lsp·bricks·ui·cocode_app 은 어느 문서에도 없다(히트 0건, 재현 확인). Discovery 95~100행이 실제로 적은 것은 3분할이다:

cocode_ade (Flutter 데스크톱 셸: 에디터·터미널·파일트리·도킹·Git)
                       ├─ agent (자체 에이전트 런타임 — 멀티 LLM의 자리)
                       └─ acp  (ACP 클라이언트 — 외부 생태계 흡수)
                    

그리고 PRD 77행이 이 도해를 "제안 계층"으로 분류하며 두 가지를 지적한다 — ⓐ 채택 결정 기록이 (PRD 작성 시점 기준) D-001~D-021에 없다 ⓑ 도해가 함께 적은 openai_dart 직접 구현은 D-017이 옵션 3으로 명시 기각했으므로 도해를 그대로 승계하면 D-017과 충돌한다.

⚠️ PRD 77행의 "D-001~D-021"을 그대로 승계하지 않는다. 의사결정 일지는 그 뒤 D-022·D-023 을 추가했고 둘 다 Impact 에 "아키텍처 문서"를 명시한다. 후속 문서는 항상 일지의 현재 마지막 번호를 확인해야 한다.

따라서 이 절은 "9분할을 채택/조정"하는 것이 아니라 3분할 제안 위에 패키지 경계를 새로 세우는 결정이다.

1.2 패키지가 존재해야 하는 조건 (DD-00)

새 패키지는 다음 중 하나 이상을 만족할 때만 만든다. 넷 다 아니면 cocode_app 내부 폴더로 둔다.

  • K1 — 다중 소비자. 워크스페이스 패키지 2개 이상이 그것을 의존한다(같은 계층이어도 된다).
  • K2 — 교체 가능한 외부 부품. 교체 가능성이 근거 문서에 명시된 외부 부품을 감싼다(예: A-03 실패 시 "자체 렌더링 텍스트 레이어 검토"로 분기 — seed-spec 152행).
  • K3 — 계층 방향. 계층 방향을 지키기 위해 계약이 아래층에 있어야 한다(package-layers 94행: "계약 인터페이스를 하위에 두고, 구현체가 자기 부트스트랩에서 스스로 등록").
  • K4 — 집행 단일화 / 구현 격리. ⓐ 한 곳에서만 강제되어야 하는 불변식을 소유하고 그 강제를 컴파일 경계로 보장해야 하거나, ⓑ 런타임 등록으로 교체되는 구현체 둘 이상이 서로를 컴파일 의존하지 못하도록 물리적으로 격리해야 한다.

rev.2 정정. 초판의 K1 은 "서로 다른 계층의 패키지 2개 이상"이었는데, 그 기준을 §1.3 표에 실제로 적용하면 11개 중 6개(신설한 2개 포함)가 탈락한다. 기준이 결론을 지지하지 못했으므로 K1 의 계층 조건을 제거하고, 실제로 작동하던 근거를 K4 로 명문화했다.

MVP 에서 feature 별 패키지 분할은 하지 않는다 — flutter-patterns Notes 131행("Cocode projects use a feature-first folder structure (lib/features/{feature_name}/)")에 따라 cocode_app/lib/features/ 폴더로 둔다. K1~K4 어느 것도 만족하지 않기 때문이다.

1.3 계층과 패키지 (DD-01)

package-layers 60행은 "리포마다 이름은 다르지만 형태는 같습니다"라고 명시하므로, cocode ADE 는 자체 사다리를 선언한다. 선언은 단일 SoT 파일(dependency_layers.yaml)에 두고 CI 스크립트가 강제한다(같은 스킬 73행·§래칫). 신규 패키지는 package-layers 172~179행 체크리스트를 통과해야 한다(루트 pubspec.yaml workspace: 등록 포함).

계층 패키지 역할 의존 가능 대상 판정 기준
L0 core 도메인 엔티티(Workspace·DelegatedTask·EditSnapshot) · 모든 계약 인터페이스 · 유즈케이스 · 정책 계산(allowed_hosts/write_exceptions 합성, 경로 술어) 워크스페이스 패키지 0개 K3
L0 ui coui_flutter 재수출(그 배럴이 coui_core 를 재수출한다 — coui-composition 121~124행) + IDE 전용 고밀도 위젯(패널·트리·탭·거터·상태바) 워크스페이스 패키지 0개 (외부 pub 만) K1(editor·terminal·cocode_app)
L0 desktop_platform 창 관리 얇은 자체 인터페이스 + 단일 창 구현 (#54). ~~네이티브 윈도잉 구현 · 창 분리·도킹~~ 은 🔄 MVP 밖(상단 「D-022 재분류」) 워크스페이스 패키지 0개. MVP 는 순수 Dart — 위젯·FFI 가 없어 dart test 로 검증되고 3-OS 파이프라인을 탄다. 네이티브 구현을 꽂을 때 Flutter·dart:ffi 를 더한다 K2 + K4ⓐ — D-022 가 "수정 지점을 한 곳으로 모은다"를 완화책으로 확정
L1 workspace 파일 IO 게이트 · EditSnapshot 파일 저장소 · edit_locks · 경로 집행 core K4ⓐ (신설)
L1 toolchain 자식 프로세스 단일 런처(샌드박스·환경변수·프록시) · dart/flutter CLI · vm_service · 자가 검증 실행기 · 파일 형식 파서 레지스트리 core K4ⓐ + K1 (신설)
L2 editor re_editor 어댑터 + 문서 버퍼 core, ui K2
L2 terminal xterm2/flutter_pty2 어댑터 core, ui, toolchain K2 (planning-inputs §4.2: "자체 구현 전에 재사용 컴포넌트에서 결함 재현 여부를 먼저 확인")
L2 lsp LSP 클라이언트 + 시맨틱 토큰 정규화 core K2(Discovery 113행이 하이라이팅 경로를 "LSP 시맨틱 토큰 정규화"로 특정)
L2 bricks package:mason 코어 인프로세스 + 스캐폴딩 원자성(§4.6) core, workspace, toolchain K4ⓐ(스캐폴딩 원자성을 한 곳에서 소유)
L2 agent 자체 런타임 — AgentRuntime 구현 core K4ⓑ
L2 acp ACP v1 클라이언트 어댑터 — AgentRuntime 구현 core, toolchain K4ⓑ
L3 cocode_app 셸·라우팅·BlocSignal 조립·런타임 등록·feature 폴더 위 전부 순수 sink
                          cocode_app (L3, sink)
                          ┌──────┬──────┬──────┬──────┬──────┬──────┐
                      editor terminal lsp bricks agent acp   (L2)
                          └──────┴──────┴──┬───┴──────┴──────┴──────┘
                                    workspace   toolchain                        (L1)
                                           └──────┬──────┘
                               core     ui     desktop_platform                     (L0)
                    

신설 3개와 그 근거:

  • workspace — C-02는 "EditSnapshot 을 남기지 않는 에이전트 편집 경로는 존재하지 않는다"를 요구한다(seed-spec 61행). 이 요구는 쓰기 지점이 하나일 때만 한 곳에서 강제된다. 없으면 agent·acp·bricks 가 각자 파일을 써서 같은 규칙이 3중 구현되고, 그 중 하나만 어긋나도 C-02가 깨진다. AC-12(잠금)·AC-14 ⓐ(경로)·AC-02(롤백)가 같은 지점에 걸린다. → K4ⓐ.
  • toolchain — AC-09는 완료 판정에서 백엔드를 구분하지 않는다(seed-spec 111행). 따라서 자가 검증기는 백엔드 밖에 있어야 두 백엔드가 같은 판정을 받는다. 동시에 AC-14 ⓑ(호스트)의 실제 집행 지점은 자식 프로세스 생성이므로(§5), 검증 실행과 샌드박스 집행이 같은 런처를 공유한다. → K4ⓐ + K1.
  • desktop_platform — D-022(오너 결정 5)가 "창 관리 호출을 얇은 자체 인터페이스 뒤에 두어 업스트림 변경 시 수정 지점을 한 곳으로 모은다"를 완화책으로 확정했다. 🔄 #100/D-025 로 구현 쪽이 MVP 밖이 되어(상단 「D-022 재분류」), 이 패키지에 남은 것은 정확히 그 완화책 한 줄이다 — 그래서 K2 근거(교체 가능한 외부 부품)는 미래형으로 읽고, 지금 성립하는 근거는 K4ⓐ(수정 지점 단일화)다. 그 단일화는 check_window_api_seal.py 가 정적으로 강제한다(#54). master 채널 @internal API 는 패치 버전에서도 breaking change 가 예고되어 있으므로 교체 가능성이 문서로 명시된 외부 부품이다(K2), 그리고 그 수정 지점 단일화가 곧 K4ⓐ다. 인터페이스와 구현을 같은 패키지에 두는 이유는 소비자가 cocode_app 과 (콜백 경유) ui 뿐이어서 계약을 더 아래로 내릴 필요가 없기 때문이다.

기각: agent 의 ACP 서버 노출(오너 결정 1). 대신 계약이 남으므로, 외부 유통이 필요해지면 AgentRuntime 위에 얇은 ACP 서버 어댑터를 나중에 얹으면 된다 — 결정 1은 문을 닫지 않는다.

1.4 순환·계층 위반이 생길 지점 (미리 지목)

package-layers 88~101행은 "하위가 상위 기능을 필요로 할 때 컴파일 의존을 만들지 말고 계약 + 런타임 등록으로 뒤집으라"고 규정한다. 아래가 그 규정을 어기고 싶어지는 지점이다. C4·C5 는 순환이 아니라 단순 하향 계층 위반이므로 유형을 분리해 적는다.

# 유형 경로 왜 생기나 해소 (DD-02)
C1 순환 editor ↔ lsp 에디터는 하이라이팅에 시맨틱 토큰이, LSP 는 didChange 에 버퍼가 필요 TextDocumentSource/SemanticTokenSink 계약을 core 에 두고 양쪽이 계약만 안다
C2 순환 agent/acp → workspace 편집을 직접 쓰고 싶어짐 금지. 런타임은 FileEditRequested 요청만 낸다(§3). 쓰기는 유즈케이스가 게이트에 위임하고 결과를 요청에 응답한다
C3 순환 acp → agent "같은 UI 경로"를 만들려고 상대 타입을 재사용 금지. 공유물은 core.AgentRuntime 뿐. 결정 1이 요구하는 바로 그 형태이며 K4ⓑ의 존재 이유다
C4 계층 위반(하향) ui(L0) → core(L0) 위젯이 DelegatedTask 를 직접 그리려 함 금지. ui 는 뷰 모델(원시 타입·전용 record)만 받는다. 매핑은 cocode_app presentation
C5 계층 위반(상향) toolchain(L1) → lsp(L2) 자가 검증 ④(정적 분석)에 LSP 진단을 쓰려 함 금지. ④의 정본은 dart analyze 프로세스 결과다(§6.1) — LSP 진단은 열린 파일에만 오므로 기준선이 흔들린다
C6 순방향(순환 아님) bricks → workspace/toolchain 스캐폴딩 원자성(AC-13)이 파일 조작 L2→L1 이므로 정상. 단 brick 훅이 임의 파일을 쓰면 게이트를 우회 → 훅 실행은 toolchain 런처 경유, mason 생성기 출력은 §4.6 의 커스텀 GeneratorTarget 경유
C7 계층 위반(하향) ui(L0) → desktop_platform(L0) window-panel 위젯이 창을 직접 조작하려 함 금지. ui 의 창 관련 위젯은 콜백만 받는다. 실제 호출은 cocode_app 이 desktop_platform 에 위임

가드(짝 필수): package-layers 99~110행은 런타임 등록에 두 가드를 필수로 붙인다 — 레지스트리 완결성 테스트, 라우트 인벤토리 스냅샷. 등록 누락은 debug 에서만 assert 되고 release 는 조용히 빈 결과이기 때문이다.

DD-02a — 완결성 테스트의 대상은 §2.2 의 계약 전수다. 초판이 3개 레지스트리만 지목한 것은 누락이었다. 완결성 테스트가 지켜야 할 것은:

  1. 계약 레지스트리 — core 가 선언한 모든 계약(§2.2 의 12종)이 cocode_app 부트스트랩에서 정확히 1회 등록되는가. 계약 목록은 core 안의 단일 상수 리스트가 SoT 이며, 소스 대조(부트스트랩 파일 파싱)와 실제 등록 함수 호출 두 축으로 검사한다(package-layers 112~116행이 두 축 분리를 요구한다).
  2. 키 레지스트리 — AgentBackendRegistry(백엔드 2종) · VerificationMethodRegistry(C-04 5종) · FileFormatParserRegistry(⑤용, §6.1) · SandboxProfileRegistry(플랫폼별).
  3. 라우트 인벤토리 스냅샷 — 셸 라우트·탭 순서·guard 소실 검출.

새 레지스트리 키를 도입하면 package-layers 108~110행대로 세 곳(소유 키 목록·모듈 함수 이름 목록·registerAll*())을 함께 갱신한다.

래칫(DD-03): 그린필드이므로 기준선을 최대 SCC 크기 1 · SCC 내부 엣지 0 · 유령 엣지 0 으로 선언하고 시작한다(package-layers 160~162행이 판단 지표를 최대 SCC 크기와 내부 엣지 수로 규정하고, 157~158행이 유령 엣지도 지표에 포함할 것을 요구한다). 증가는 CI 가 거부하며 완화는 사람이 선언 파일을 고치고 PR 에 근거를 남긴다(같은 스킬 169~170행). 재수출되는 패키지를 소비 측 pubspec 에 다시 적지 않는다 — 실측에서 이 중복이 유령 의존 39개의 원인이었다(같은 스킬 84~86행). 범용 dependency_validator 는 게이트로 쓰지 않는다(같은 스킬 164~167행).


2. (b) 레이어 구성 — Clean Architecture 를 IDE 도메인에 매핑

flutter-patterns 41~43행이 규정하는 3층(Domain: Entity/UseCase/Repository Interface · Data: Repository Impl/DataSource/Model · Presentation: BLoC/Page/Widget)을 그대로 쓴다. 패키지 계층과 레이어는 별개 축이다(package-layers 182행).

두 가지 조직 규칙을 명시적으로 승계한다(초판 누락):

  • BLoC 은 Repository 를 직접 참조하지 않고 UseCase 를 통해 호출한다(flutter-patterns 132행).
  • 불변 State 는 Freezed 로 생성한다(같은 문서 135행). 이는 §2.3 의 "상태는 불변 + 의미 있는 ==" 요구와 같은 장치다. ⚠️ planning-inputs §3.2 는 참조 워크스페이스 org 전체에 .freezed.dart 가 0개라고 기록하므로, 이 규칙은 cocode ADE 신규 코드에 적용되는 것이지 참조 워크스페이스의 형태가 아니다.

2.1 엔티티 3종의 거처 (DD-04)

엔티티 Domain Data Presentation
Workspace 엔티티 + 불변식 3개(seed-spec 73~75행). allowed_hosts·write_exceptions 합성 규칙(기본+파생+사용자)도 도메인 순수 함수 WorkspaceConfigStore(.cocode/config/workspace.json), 의존성에서 호스트 추출(§5), brick 적용 이력 워크스페이스 선택·설정 화면
DelegatedTask 엔티티 + 상태 전이 함수. status 7종·completion_mode·pending_approval_kind 조합의 전이는 도메인 함수 하나만 통과한다 작업 영속화(.cocode/history/tasks/), verification_log 직렬화 작업 목록·상세·승인 UI
EditSnapshot 엔티티 + 불변식 4개(seed-spec 92~95행). 롤백 대상 집합 산출·충돌 판정은 도메인 순수 함수 파일 기반 저장소(§4) — blob·인덱스·저널 diff 뷰(base/result 에서 파생)

판정을 없애는 규칙 (DD-05): status·pending_approval_kind 를 Data·Presentation 이 직접 대입하지 않는다. 전이는 TaskTransition 도메인 함수만 수행하고, 그 함수는 불변식 1~5(seed-spec 82~86행)를 자기 안에서 검사한다. 이렇게 하면 "어디선가 status 를 바꿨다"가 lint 로 잡힌다(bloc_signals_lint 와 같은 방식으로 custom_lint 규칙 1개 추가).

📌 구현 주석 (#17, 2026-08-29) — 집행 수단은 custom_lint 가 아니다. 위 문장의 전제(「조직에 bloc_signals_lint 선례가 있다」)가 이 리포에는 성립하지 않는다. 실측: pubspec.yaml·analysis_options.yaml·.github/workflows/** 전체에서 custom_lint 문자열 0건, 워크스페이스에 custom_lint_builder 의존 패키지 0개, bloc_signals_lint 패키지 0개. analyze:select 는 dart analyze --fatal-infos lib && dcm analyze … lib 이라 custom_lint 단계가 아예 없다 — 규칙을 써도 CI 에서 실행되지 않는다. 이 리포의 실제 선례는 .github/scripts/check_*.py 가드 ~50개이고, 그중 check_bloc_signals_lookup.py 가 「컴파일도 정적분석도 잡지 못한다」는 같은 문제군을 다룬다. 그래서 DD-05 의 집행은 check_task_status_assignment.py(CI changes 잡, 회귀 테스트 test_check_task_status_assignment.py)가 한다. 결정 자체(전이 지점 단일화)는 바뀌지 않는다 — 바뀐 것은 그것을 기계적으로 강제하는 수단뿐이다. ⚠️ 같은 전제를 인용하는 DD-15(#22 · #31)도 착수 시 이 사실을 먼저 확인할 것.

⚠️ pending_approval_kind 는 폐쇄 집합이다(없음 | 완료승인 | 테스트변경승인 — seed-spec 78행). §3 머리말이 "잠금 후 기존 엔티티 수정 불가, 추가만 허용"을 규정하므로, 이 문서는 새 승인 종류를 도입하지 않는다. 그 제약이 §5.3 DD-17 의 결론을 바꾼다.

2.2 계약 인터페이스 목록 (전부 core)

AgentRuntime(§3) · SnapshotStore · WorkspaceFileGate · EditLockRegistry · VerificationRunner · FileFormatParser · ProcessLauncher · SandboxPolicy · BrickEngine · TextDocumentSource · SemanticTokenSink · CredentialVault — 12종. 이 목록은 core 안의 단일 상수 리스트를 SoT 로 두며, §1.4 DD-02a 의 완결성 테스트가 전수를 검사한다.

구현은 L1/L2 에 있고 등록은 cocode_app 부트스트랩에서 한다. 등록 컨테이너는 core 가 소유하는 자체 타입드 레지스트리(계약 타입 → 구현 인스턴스)로 하고 범용 서비스 로케이터 패키지를 쓰지 않는다 — 완결성 테스트가 키 목록을 열거할 수 있어야 하기 때문이다.

2.3 Presentation — BlocSignal (오너 결정 2)

  • 패키지: bloc_signals(순수 Dart 코어) · bloc_signals_flutter(Provider/Builder) · bloc_signals_test · bloc_signals_lint(bloc-patterns 17~20행).
  • 세 가지 성질을 설계 전제로 삼는다: emit 이 동기(같은 실행 블록에서 반영 — 31~37행), 읽기는 stateValue(42~49행), 동일 상태는 자동 dedup(53행: emit(next) is a no-op when next == stateValue → mutate-in-place 는 갱신을 조용히 삼킨다 → 상태는 불변 + 의미 있는 ==).
  • DI 는 Pure DI(BlocSignalProvider 직접 생성 — bloc-patterns 442~450행, flutter-patterns Actions 4).
  • 고빈도 이벤트는 Bloc 을 통과시키지 않는다 (DD-06): 텍스트 버퍼 변경·터미널 출력·스크롤은 editor/terminal 내부 상태로 처리하고, Bloc 은 문서 열기/저장/커서 요약/작업 상태 같은 저빈도 사건만 다룬다. 근거: AC-15 는 10만 줄 파일에서 "조작이 입력한 순서대로 반영되고 화면이 응답하지 않는 구간이 발생하지 않는다"를 요구하며(seed-spec 117행), A-03 은 MVP 게이팅 리스크다. 상태 전파 계층을 키 입력마다 태우면 그 리스크를 우리가 키운다. 이는 flutter-patterns 의 "Presentation: BLoC" 로부터의 의도적 이탈이며 근거를 여기 남긴다.

2.4 UI — CoUI 기반 + IDE 전용 확장 (오너 결정 4)

coui-composition-and-extension 58~74행의 사다리를 그대로 따른다: ① 그대로 사용 → ② 단일 Core<X>Style 슬롯 오버라이드 → ③ 합성(동작은 CoUI, 콘텐츠·크롬은 우리 것) → ④ CoUI 를 하위호환 추가로 확장(별도 저장소 PR 이 먼저 머지) → ⑤ 커스텀 위젯(최후, 사유 기록). 첫 번째로 통하는 단에서 멈춘다.

화면 요소 사다리 위치 비고
다이얼로그·버튼·입력·토스트·탭·트리·리사이저블·커맨드 팔레트 ①~② cc-coui 스킬에 coui-tree·coui-resizable·coui-command·coui-code-diff·coui-code-snippet·coui-window-panel·coui-scroll-area·coui-table·coui-tabs·coui-sortable 이 실재함을 디렉터리로 확인했다 — 먼저 그것을 확인하고 없을 때만 올라간다
diff 뷰어 ②~③ coui-code-diff/coui-diff 둘 다 실재 — 확인 후 판단
창 분리·도킹 크롬 ③ + desktop_platform 🔄 MVP 밖(상단 「D-022 재분류」) — MVP 셸은 단일 창이고(ux-spec §2.3) 분리·도킹 호출은 WindowOperationUnsupported 를 돌려준다. 아래는 네이티브 구현이 꽂힌 뒤의 서술이다. 오너 결정 5(D-022). 패널 크롬은 coui-window-panel 합성, 실제 창 생성·분리·네이티브 핸들 조작은 desktop_platform 인터페이스 호출. ui 는 콜백만 받는다(C7)
코드 에디터 표면·거터·미니맵·터미널 뷰 사다리 밖(외부 패키지 통합) re_editor·xterm2/flutter_pty2 는 커스텀 위젯이 아니라 서드파티 패키지 어댑터다. CoUI 사다리는 자체 구현 여부를 다루는 축이므로 여기에 적용되지 않는다. 사유: 텍스트 렌더링·IME·가상 스크롤은 CoUI 의 계약 범위 밖이며 AC-15/AC-16 이 직접 거는 표면이다. A-03·A-04 스파이크 결과에 따라 자체 렌더링으로 분기할 수 있다(seed-spec 152·153행)

ui 는 ①~③과 자체 위젯을 담고, ④(CoUI 자체 확장)는 cocode ADE 저장소가 아니라 CoUI 저장소(coco-de/coui) PR 로 나가며 먼저 머지된다(coui-composition 361~379행: 소비 화면 PR 에는 CoUI 소스 diff 가 0이어야 하고, coui_flutter 와 coui_web 를 같은 PR 에서 함께 고친다). 모든 신설 위젯은 knob 기반 Widgetbook use-case 를 동반한다(같은 스킬 401~407행).


3. (c) 에이전트 런타임 계약

3.1 계약 (DD-07)

core 에 두 백엔드가 공유하는 인터페이스를 둔다. 직렬화는 없다(결정 1).

rev.2 정정 — 요청-응답 채널을 추가했다. 초판은 Stream<AgentEvent> 단방향 + send(UserTurn) 만 두었는데, 그러면 FileReadRequested 에 파일 내용을 돌려줄 수단이 없다. §3.3 이 채택한 ACP 클라이언트 fs 능력(fs/read_text_file·fs/write_text_file)과 terminal 능력은 전부 JSON-RPC 요청-응답이므로, 초판 계약으로는 Gemini CLI(파일 IO 를 클라이언트로 되돌림 — planning-inputs §5.1)를 연결할 수 없어 AC-06 ⓑ가 실제로 충족되지 않는다.

enum AgentBackendKind { cocodeAgent, cocodeAcp }          // DelegatedTask.agent_backend_kind
                    
                    abstract interface class AgentRuntime {
                      AgentBackendKind get kind;
                      Future<List<AgentProvider>> describeProviders();     // AC-06 의 선택지 원천
                      Future<AgentSession> startSession(SessionRequest request);
                    }
                    
                    abstract interface class AgentSession {
                      AgentCapabilities get capabilities;   // loadSession / resume / fork 유무
                      Stream<AgentEvent> get events;        // 이 세션에 대한 유일한 관찰 지점
                      Future<void> send(UserTurn turn);
                      Future<void> cancel(CancelReason reason);
                      Future<void> dispose();
                    }
                    
                    /// 에이전트가 알리기만 하는 사건.
                    sealed class AgentNotification implements AgentEvent {}
                    final class MessageDelta   extends AgentNotification {}   // 사용자에게 보여줄 출력
                    final class TurnEnded      extends AgentNotification {}   // 정상 종료 사유
                    final class TransportClosed extends AgentNotification {}  // 전송 계층 확정 종료 (§6.2)
                    
                    /// 에이전트가 **응답을 기다리는** 요청. 호스트가 반드시 complete 해야 한다.
                    sealed class AgentRequest<R> implements AgentEvent {
                      String get requestId;
                      /// 호스트(유즈케이스)가 결과를 돌려주는 유일한 통로.
                      /// 미완료 상태로 두면 §6.2 S2 가 유휴로 오검출하므로, 유즈케이스는
                      /// 반드시 성공·거부·오류 중 하나로 완료시킨다.
                      void complete(R result);
                      void fail(AgentRequestError error);
                    }
                    
                    final class FileReadRequested     extends AgentRequest<FileReadResult> {}   // 경로 → 내용
                    final class FileEditRequested     extends AgentRequest<FileEditResult> {}   // 경로 + 새 내용/패치 → 적용/거부/충돌
                    final class CommandRequested      extends AgentRequest<CommandResult> {}    // argv + cwd → exit code·출력
                    final class PermissionRequested   extends AgentRequest<PermissionResult> {} // 승인 종류 → 허용/거부
                    
                    sealed class AgentEvent {}
                    
  • AgentRequest.complete/fail 은 호스트만 호출한다. 백엔드 구현은 그것을 자기 전송(ACP JSON-RPC 응답 / 인프로세스 도구 반환값)으로 되돌린다.
  • 모든 AgentRequest 에는 §6.2 S2 와 별개인 요청 수준 타임아웃이 걸린다. 값은 §9 미결 5 와 같은 취급.

📌 결정 주석 (#71, 2026-09-13 — D-059) — CancelReason 폐쇄 값 집합 5종. 초판은 cancel(CancelReason reason) 만 적고 값 집합을 두지 않아 호출부를 쓸 수 없는 죽은 API 였다(docs/ 전수 grep 히트 = 선언부 1건). 값은 호출 지점 하나에 하나씩 대응하며 자유 서술을 받지 않는다 — userStopped(AC-22 중지, 전이 ⑰·⑱) · userRolledBack(진행 중 작업의 롤백 요청, ⑦·⑫) · approvalRejected(AC-23 거부, ⑲) · taskFailed(실패 종결 정리, ②·⑥·⑪) · workspaceClosing(워크스페이스·앱 종료 — 전이 아님). 호출 순서는 전이가 먼저, cancel 이 나중이다(docs/stop-and-limits-cocode.md §4.2 — 전이가 거부되면 세션을 죽이지 않는다). cancel 은 멱등이며 이미 죽은 세션(S1 이후)에 대한 호출도 오류가 아니다.

3.2 계약에 없는 것과 그 이유

계약 밖 어디에 있나 왜
파일 쓰기 실행 workspace 게이트 C-02 — 쓰기 지점이 둘이면 EditSnapshot 없는 경로가 생긴다. 계약은 요청만 전달하고 결과를 돌려받는다
자가 검증 실행 toolchain AC-09 가 백엔드를 구분하지 않는다
완료·실패 전이 core 도메인 함수 불변식 1~5는 백엔드와 무관하다
파일 잠금 workspace AC-12·AC-18
샌드박스 toolchain 런처 AC-14 는 "에이전트가 작업을 수행할 때"에 걸린다

설계 진술: 런타임 계약은 에이전트가 무엇을 하고 싶어하는지를 전달하고 그 결과가 무엇이었는지만 돌려받는다. 무엇이 실제로 일어나는지를 정하는 정책은 전부 계약 바깥의 공통 경로에 있다. 정책을 계약 안에 하나라도 넣으면 두 구현이 서로 다르게 만들 수 있고, 그 순간 AC-02·AC-09·AC-12·AC-14 가 백엔드별로 갈린다.

3.3 두 백엔드의 매핑

계약 요소 agent(인프로세스) acp(외부 프로세스, JSON-RPC 2.0 over stdio — seed-spec 22행)
describeProviders() Anthropic(anthropic_sdk_dart) + OpenAI 호환 게이트웨이 (D-017) 연결된 ACP 에이전트 목록. 검증 대상 4종은 Claude Agent·Codex·Gemini CLI·Copilot CLI (D-019, planning-inputs §5.1)
capabilities 고정 에이전트별로 다름 — Claude Agent(loadSession+list+fork+resume) / Codex(resume 있고 fork 없음) / Gemini CLI(loadSession만) / Copilot CLI(네이티브 서버) (planning-inputs §5.1). 능력 차이의 graceful degradation 규칙은 ~~PRD D-10 미결 → §9 미결 13~~ D-063(#51) 로 확정 — 비활성 + 사유, 자동 대체 없음(acp-integration-design-cocode.md §3)
FileEditRequested / FileReadRequested 도구 호출 → complete() 반환값이 도구 결과 클라이언트 fs 능력(fs/read_text_file·fs/write_text_file). Gemini CLI 는 파일 IO 를 클라이언트로 되돌리므로 이 구현을 강제한다(같은 절)
CommandRequested 도구 호출 ACP terminal 능력을 클라이언트가 제공한다 — 그래야 실행이 우리 런처를 탄다
TransportClosed HTTP 스트림 종료·인증 거부 자식 프로세스 exit, stdio EOF

3.4 AC-06 충족 확인

AC-06 원문(seed-spec 108행)은 "자체 런타임(agent — 서로 다른 LLM 프로바이더 2개 이상)과 외부 ACP 에이전트 호스팅(acp — ACP 에이전트 1개 이상 연결) 두 경로가 모두 제공된다"이다. BDD Feature 3 의 두 시나리오는 ⓐ agent_backend_kind = cocode_agent 선택 시 프로바이더 2개 이상 제시 ⓑ cocode_acp 선택 시 외부 에이전트 1개 이상 연결이다(bdd 310~325행).

  • ⓐ는 AgentRuntime.describeProviders() 가 그대로 UI 선택지가 되므로 충족된다. 구체 조합(D-017)은 인수 조건의 값이 아니다(bdd 312~313행이 명시).

    구현에 어떻게 반영됐나 (#45). CocodeAgentRuntime 은 프로바이더를 주입받는다 — LlmProviderRegistry 를 생성자로 받고 조합을 코드에 박지 않는다. 레지스트리는 서로 다른 프로바이더가 2개 미만이면 만들어지지 않으므로(생성 시점 거부) 「선택지가 하나뿐인 채 출하」가 구조적으로 불가능하고, 동시에 어떤 조합인지는 이 층 어디에도 없다. 그래서 Anthropic + OpenAI 호환 게이트웨이를 다른 조합으로 바꿔도 AC-06 ⓐ의 판정은 달라지지 않는다 — 바뀌는 것은 부트스트랩이 넘기는 목록뿐이다. OpenAI 쪽 어댑터가 벤더가 아니라 규격(baseUrl 필수)인 것도 같은 이유다.

  • ⓑ는 acp 의 AgentRuntime 등록 + describeProviders() + §3.1 의 요청-응답 채널로 충족된다. 채널이 없으면 클라이언트 fs 를 요구하는 에이전트(Gemini CLI)가 연결되지 않는다.

  • 결정 1이 AC-06 을 위반하지 않음: AC-06 은 노출 방식을 규정하지 않는다. Discovery 도해의 "스스로도 ACP 서버로 노출"은 제안이며 채택 기록이 없다(PRD 77행).

한계 하나를 명시한다. AC-06 의 ⓑ가 요구하는 것은 "연결"이며, 연결된 에이전트가 C-02 를 지킬지는 AC-06 이 다루지 않는다. 클라이언트 fs 능력을 무시하고 스스로 파일을 쓰는 ACP 에이전트가 있으면 EditSnapshot 없는 편집 경로가 생긴다. 대응은 §5.4, 어긋남 기록은 §10-②.


4. (d) EditSnapshot 저장 설계

4.1 디렉터리 레이아웃 (DD-08)

<workspace>/.cocode/
                      config/workspace.json                        # allowed_hosts · write_exceptions · dart_sdk_mode
                      locks/edit_locks.json                        # AC-12 / AC-18 (리스 방식, §4.5)
                      history/
                        objects/<sha256[0:2]>/<sha256>             # content-addressed blob (gzip)
                        files/<path_key>/versions.jsonl            # 파일별 버전 체인 (append-only)
                        tasks/<task_id>/snapshots.jsonl            # 작업별 스냅샷 순서 (sequence_no)
                        journal/<op_id>.json                       # 롤백 2단계 커밋 저널 (§4.3 DD-11)
                        journal/writes/<op_id>.json                # 쓰기 커밋 의도 저널 (§4.3a DD-26)
                    

DD-08a — path_key 정규화 (rev.2 신설). path_key = sha256(normalize(워크스페이스 상대 경로)) 이며 normalize 는 다음 순서로 정의한다:

  1. 경로 구분자를 / 로 통일한다.
  2. 유니코드 NFC 정규화를 적용한다(macOS 가 NFD 로 돌려주는 자모 분리 대응 — AC-16 이 한글을 1급으로 다루므로 파일명에 한글이 오는 경우가 실제로 있다).
  3. 대상 볼륨이 대소문자 무시면 소문자로 폴드한다. 볼륨 특성은 워크스페이스 열 때 1회 실측해 세션 값으로 고정한다(플랫폼 하드코딩 금지 — Linux 에서도 대소문자 무시 마운트가 가능하다).
  4. versions.jsonl 첫 줄에 정규화 전 원본 상대 경로를 적어 역인덱스를 만든다.

⚠️ 초판은 "대소문자 차이를 파일명으로 끌고 오지 않기 위해서"를 근거로 원시 경로를 해싱했는데, SHA-256 은 대소문자를 보존하므로 그 근거가 결과와 반대였다. 정규화 없이 두면 대소문자 무시 파일시스템(Windows NTFS 기본, macOS APFS 기본)에서 같은 파일이 두 개의 버전 체인을 갖고, §4.3 의 "그 파일의 마지막 스냅샷" 조회가 비어 충돌 판정이 통째로 빠진다. C-01 이 3플랫폼을 Must 로 잠갔으므로 이 규칙은 필수다.

  • 위치를 워크스페이스 안으로 잡은 이유: write_exceptions 는 "워크스페이스 밖" 경로 집합이므로(seed-spec 70행), 히스토리를 밖에 두면 예외 목록이 늘어난다. 안에 두면 AC-14 ⓐ 판정이 그대로 통과한다.
  • 대가와 그 처리: 워크스페이스 안이면 에이전트도 쓸 수 있다(AC-14 ⓐ는 워크스페이스 내부를 허용한다). 그러면 에이전트가 자기 흔적을 지울 수 있다. → DD-09: 게이트는 <workspace>/.cocode/** 로의 에이전트 쓰기를 항상 거부한다. 경로 술어이므로 판정이 없다. AC-14 를 좁히는 것은 위반이 아니다 — AC-14 는 "수정하지 않는다"만 요구하며 더 적게 수정하는 것을 금지하지 않는다.

4.2 두 해시를 어떻게 보관하는가 (v3.1.0 신설분)

v3.1.0 이 result_content_hash 를 신설하고 충돌 판정을 "현재 해시 ≠ 그 파일의 마지막 EditSnapshot 의 result_content_hash" 로 바꿨다(seed-spec 93행). base_content_hash 는 복원 목표다.

DD-10 — content-addressed 저장. blob 파일명이 곧 내용 해시(SHA-256, 외부 표준 FIPS 180-4)다. 스냅샷 레코드는 다음만 갖는다:

{"snapshot_id":"…","task_id":"…","seq":3,"attempt":1,"path":"lib/foo.dart",
                     "base":"sha256:…","result":"sha256:…","ts":"…"}
                    
  • attempt 는 seed-spec §3 의 attempt_no 다 — 그 편집을 남긴 시도의 번호(최초 0, 재시도마다 1 증가). seq 와 역할이 다르다: seq 는 작업 전체에서 몇 번째 편집인가(시도를 가로질러 단조 증가), attempt 는 그 편집이 어느 시도에 속하는가다. 재시도는 직전 시도의 편집을 되돌리지 않으므로(D-030 — 누적) 최대 4세트(최초 1 + 재시도 3)가 한 작업 안에 쌓이고, attempt 없이는 그 경계를 사후에 복원할 방법이 없다.

  • base/result 는 해시이자 blob 주소다. 따라서 두 해시의 "보관"과 "복원 원본의 보관"이 같은 한 가지 일이 된다.

  • 같은 내용은 자동으로 한 번만 저장된다(dedup). A→B→A 편집은 blob 2개다.

  • diff 속성은 저장하지 않고 base/result blob 에서 파생한다. Seed Spec §3 은 diff 를 속성으로 규정할 뿐 물리 저장을 요구하지 않는다.

  • 첫 편집의 복원 원본이 반드시 존재한다: 게이트가 쓰기 전에 원본을 blob 으로 넣고 base 를 채우므로, 파일별 최소 sequence_no 스냅샷의 base blob 이 곧 "작업 시작 직전 상태"다(불변식 4, seed-spec 95행).

4.3 원자적 롤백 (DD-11)

롤백은 여러 파일 쓰기다. 파일시스템에 다중 파일 원자 커밋은 없으므로, 재개 가능한 원자성으로 정의한다 — 중간 상태를 사용자에게 확정된 것으로 보여주지 않는다.

  1. 대상 산출(도메인 순수 함수) — 입력은 (스냅샷 레코드, 현재 해시, **롤백 질의**) 이며, 롤백 질의가 경계를 지정한다: 특정 스냅샷이면 그 이후 같은 작업의 모든 스냅샷, 시도 경계(attempt 값 k)면 attempt ≥ k 인 스냅샷, 작업 시작 직전이면 그 작업의 모든 스냅샷(불변식 3·4). 세 질의는 seed-spec §3 EditSnapshot 「시도 경계 롤백」의 ⓐⓑⓒ 에 대응하며, 각각 대상 집합이 유일하게 결정된다 — 「작업 시작 직전」은 시도 경계 질의에 k=0 을 넣은 것과 같다. ⚠️ ⓑ(시도 k 가 남긴 편집만)는 k 가 마지막 시도가 아니면 그 위에 쌓인 편집이 남아 원자성이 깨지므로 충돌로 제시한다. 함수의 시그니처·구현은 #24 소유이며 이 절은 그 입력 요구만 고정한다.
  2. 사전 검사 (rev.2 정정) — 각 대상 파일에 대해 두 가지를 모두 본다.
    • ⓐ 그 파일의 전역 마지막 EditSnapshot 이 롤백 대상 작업의 것인가. 아니면(다른 작업 T2 가 그 뒤에 편집했다면) 충돌이다.
    • ⓑ 현재 내용 해시 == 롤백 대상 작업이 그 파일에 대해 남긴 마지막 스냅샷의 result 인가. 다르면 충돌이다.
    • 하나라도 불일치하면 아무 파일도 건드리지 않고 충돌로 제시한다(BDD 183행 "대상 파일 중 하나라도 충돌하면 어떤 파일도 되돌아가지 않는다").

    ⚠️ 초판은 ⓑ만 두되 비교 대상을 "그 파일의 마지막 스냅샷"으로 적었다. 그러면 T1 편집 → T2 편집 후 T1 을 롤백할 때 현재 해시가 T2 의 result 와 같아 충돌 없이 통과하고 T2 의 편집이 조용히 소실된다. AC-12 는 동시 편집만 금지하므로 이 순서는 합법이며, AC-02 의 보호가 무력화된다. ⓐ가 그 경로를 막는다.

  3. 준비 — 각 목표 내용을 같은 볼륨의 임시 파일에 쓰고 fsync.
  4. 커밋 의도 기록 — journal/<op_id>.json 에 (경로, 목표 해시, 임시 파일) 목록을 쓰고 fsync.
  5. 치환 — 순차 원자적 치환. POSIX rename(2) 은 같은 파일시스템에서 원자적이다(외부 표준). Windows 도 Dart File.rename 그대로 쓴다(D-040, §9 미결 3 해소 — docs/windows-atomic-replace-cocode.md): Dart 의 Windows 구현이 곧 MoveFileExW(MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH) 이고(SDK 3.13.0 소스), windows-2022 실측 2회에서 같은 볼륨·대상 핸들 0개 조건 아래 이름 공간 기준 원자성이 확인됐다(동시 읽기 13,056회에서 빈/부분/혼합 내용 0건). FFI 는 쓰지 않는다 — ReplaceFileW 는 치환 중 대상 이름이 사라지는 창(읽기의 9.5%)이 관측돼 채택 불가이고, POSIX-semantics rename 은 홀더가 FILE_SHARE_DELETE 를 준 경우에만 이득이라 조건부 옵션(W-6)으로만 남긴다. 핸들 점유 시: 대상에 핸들이 하나라도 있으면(공유 모드·프로세스 무관) ERROR_ACCESS_DENIED(5), 임시 파일 쪽에 delete 공유 없는 핸들이 있으면 ERROR_SHARING_VIOLATION(32) 이며 두 경우 모두 파일은 건드려지지 않는다. Dart 자신의 File.open 핸들은 delete 공유가 없어 모든 치환 API 를 막으므로 치환 직전 자기 핸들이 0개여야 한다. 구현 계약 W-1~W-7(같은 볼륨 임시 파일 · 5/32 는 일시 점유로 재시도 후 충돌 제시, 단 5 는 읽기 전용·디렉터리 대상을 먼저 걸러낸다 · 읽기 경로의 32 재시도)은 그 문서 §4.4 가 정본이며 #25 가 구현한다.
  6. 완료 기록 후 저널 삭제.
  7. 재기동 시 남은 저널을 재수행한다. 각 항목은 멱등이다 — 현재 해시가 이미 목표 해시면 건너뛴다.
  8. 롤백 중 잠금 (rev.2 정정) — 롤백은 사용자 조작이므로 edit_locks 리스의 소유자가 될 task_id 가 없다. 따라서 롤백은 edit_locks 를 점유하지 않고, workspace 게이트가 op_id 를 키로 한 별도의 배타 구간(rollback_in_progress)을 잡는다. 게이트는 그 구간에 걸린 경로에 대한 모든 에이전트 쓰기를 거부하며, 구간은 6단계 완료 또는 7단계 재수행 완료로 해제된다. Seed Spec Workspace 불변식 2 는 DelegatedTask 사이의 배타만 규정하므로 이 구간은 그 불변식과 별개의 장치다.

📌 구현 주석 (#25, 2026-09-11) — workspace 의 RollbackEngine(lib/src/rollback/)이 2~8단계를 수행한다. 구현의 단계 순서는 저널 → 스테이징 → 치환이다(위 3·4단계의 순서를 바꿈): 저널 history/journal/<op_id>.json(임시 파일 + flush + rename 으로 원자 기록, RollbackJournalStore — 항목마다 직전 상태·목표 해시·스테이징 경로)을 먼저 쓰고, 스테이징은 대상과 같은 디렉터리의 .<name>.cocode-rollback-<op_id>-<n>.tmp(같은 볼륨 — W-1 · 대상이 있으면 File.copy 로 만들어 실행 비트 상속)에 만든다. 저널이 스테이징에 선행하는 이유: 저널 없는 스테이징 잔여물이 생기지 않고(저널이 곧 잔여물의 명단), 재수행이 스테이징을 blob 에서 다시 만들 수 있다 — 치환 직전에 스테이징의 해시를 목표와 대조해 다르거나 없으면 blob 에서 다시 만든다. 2~6단계 전체는 게이트의 직렬 구간(FileWorkspaceGate.runSerially) 안에서 돌아 사전검사와 치환 사이에 에이전트 쓰기가 끼어들 수 없고, 배타 구간은 대상 경로와 스테이징 경로 전부에 걸린다. 재기동 직후에는 게이트가 첫 연산 전에 남은 저널의 경로를 배타 구간에 다시 잡는다(스캔 실패 시 무장 표시를 하지 않아 다음 연산이 다시 시도한다). 첫 치환 전에 막히면(그 사이 게이트 밖에서 고침·일시 점유 소진·준비 실패) 저널·스테이징을 걷고 충돌/실패로 답한다 — 바뀐 것이 없으므로 구간을 잡아 두지 않는다. 하나라도 치환된 뒤에 막히면 저널을 남겨 재수행이 잇는다. 재수행은 현재가 직전 상태도 목표도 아니면(그 사이 누가 고침) 적용하지 않고 미완료로 보고하며 저널·구간을 유지한다 — 사람이 그 상태를 받아들이면 RollbackEngine.abandonPendingJournal(op_id) 로 저널을 버리고 구간을 푼다. 항목 순서는 부재(삭제) 먼저·복원 나중이다 — 대소문자 무시 볼륨의 Foo.dart → foo.dart 되돌림처럼 두 경로가 한 파일을 가리킬 때 복원 뒤 삭제하면 방금 복원한 파일이 지워진다. 그런 볼륨에서 실측은 철자를 따지지 않는다(같은 path_key 는 같은 파일 — 게이트가 lib/a.dart 로 기록한 편집이 디스크에는 A.dart 로 남는 일이 정상이다). 예외는 같은 연산 안에 같은 키의 두 철자가 있을 때뿐이며 그때만 디렉터리 목록의 실제 철자로 판정한다. 7단계 재수행 진입점은 RollbackEngine.replayPendingJournals() 이며 쓰기 저널 복구와의 실행 순서는 §4.3a(DD-26, #31) 가 규정한다. 8단계의 배타 구간은 RollbackExclusionRegistry(게이트와 같은 인스턴스, 프로세스 메모리 — 재기동 시 재수행이 다시 잡는다)이고 게이트는 걸린 경로의 쓰기를 FileEditConflict.rollbackInProgress 로 돌려준다. W-3 의 재시도 값은 ReplaceRetryPolicy(출하 기본 5회 × 20ms — 근거는 그 클래스 doc: S8 실측 치환 지연 p95 < 1ms 의 100배가 넘는 창) 파라미터다. 해석 불가 저널(형식·해시·op_id≠파일명·.cocode/** 경로)은 지우지 않고 보고한다(판정 불가 ≠ 통과). 치환·삭제 원시 함수(replaceFileAtomically·replaceWithRetry·removeWithRetry)는 패키지 밖으로 내보내지 않으며 DD-15 가드가 그 이름의 호출을 게이트 우회로 잡는다.

4.3a 쓰기 커밋 프로토콜과 재기동 복구 (#31) — DD-26

§4.3 DD-11 은 롤백 쪽 원자성만 규정한다. 게이트가 파일을 실제로 쓰는 순간과 그 편집의 EditSnapshot 을 .cocode/history 에 확정하는 순간 사이의 크래시 창(flow-permutation C-7 · FP-109 · B-10)은 이 절이 닫는다. 값은 정하지 않는다 — 순서·저장 위치·판정 규칙만 정한다.

저장 위치·파일명·스키마. 쓰기 저널은 history/journal/writes/<op_id>.json 이다(§4.1 레이아웃에 추가). 롤백 저널과 디렉터리를 나누는 이유는 두 저널의 스키마·복구 규칙·실행 순서가 달라 한 디렉터리에 섞이면 스캔이 서로의 파일을 손상으로 읽기 때문이다. 스키마(v=1):

{"v":1,"op_id":"…","task_id":"…","attempt":0,"created_at":"…",
                     "records":[{"snapshot_id":"t1-3","task_id":"t1","seq":3,"attempt":0,
                                 "path":"lib/a.dart","base":"sha256:…","result":"sha256:…","ts":"…"}]}
                    

항목은 append 될 레코드 그대로다(순번·식별자까지 확정된 값) — 저널이 곧 레코드의 선행 사본이며, 파일 조작은 레코드의 종류(수정·생성·삭제·이름변경 — 불변식 6)에서 파생된다. 디렉터리 분해(R5-3)는 항목 N건이 한 저널이다. 저널 스캔은 롤백 저널과 같은 규칙으로 손상을 판정한다(형식·op_id≠파일명·빈 항목).

커밋 프로토콜(게이트 쓰기 경로의 정본 — #22 는 이 순서를 따로 정의하지 않는다).

  1. 순번 확보 — 작업의 다음 seq 와 attempt 단조성 검사. 여기서 실패하면 아무것도 바뀌지 않는다.
  2. blob 적재 — base(직전 내용)와 result(쓸 내용)를 둘 다 파일을 건드리기 전에 content-addressed 저장소에 넣는다(각각 임시 파일 + flush + rename — DD-10 의 「쓰기 전에 원본을 blob 으로」를 결과까지 확장). 어느 경계에서 죽어도 레코드가 가리키는 blob 이 존재한다.
  3. 커밋 의도 저널 — 레코드 전체를 담은 저널을 임시 파일 + flush + rename 으로 기록. 이 순간부터 편집은 복구 가능하다.
  4. 파일 조작 — 항목 순서대로 원자적 치환(같은 볼륨 임시 파일 + File.rename, W-1~W-7) · unlink · rename.
  5. 레코드 append — 항목 순서대로 두 인덱스(tasks/<task_id>/snapshots.jsonl → files/<path_key>/versions.jsonl)에 flush 하며 쓴다.
  6. 저널 삭제.

재기동 복구 — 판정 규칙(수렴 방향). 남은 쓰기 저널의 항목마다 저널의 레코드(base·result 해시)와 디스크의 현재 상태만 본다 — 추측이 없다:

관측 판정 왜
레코드가 두 인덱스에 이미 있다 alreadyRecorded — 할 일 없음 5단계 뒤에 죽었다
파일이 result 상태(이름변경은 두 경로 모두) completed — 빠진 인덱스에만 레코드 append(멱등) 4단계 뒤·5단계 전에 죽었다. 저널은 파일을 건드리기 전에 쓰였으므로 이 상태는 게이트의 편집이 실제로 착지한 것이다 — 레코드를 확정하는 것은 합성이 아니라 이미 정해진 레코드의 기입이다
파일이 base 상태 discarded — 레코드를 남기지 않고 이 항목과 뒤 항목 전부 폐기 4단계 전에 죽었다. 편집이 일어나지 않았다(조작은 순서대로이므로 뒤 항목도 일어나지 않았다)
둘 다 아니다 unresolved — 파일도 레코드도 건드리지 않고 보고, 저널은 남긴다 죽은 사이 누군가 파일을 바꿨다. 판정 불가는 통과가 아니고, 자동 수렴은 사람의 편집을 덮는다

이 규칙으로 #31 AC 7 의 「ⓐ base 로 되돌린다 / ⓑ 사후 EditSnapshot 을 합성한다」 양자택일은 해소된다 — 저널이 있으면 ⓑ 는 합성이 아니라 확정이고, ⓐ 로 되돌릴 근거(파일이 result 인데 레코드를 버릴 이유)는 없다. 저널 없이 파일만 바뀐 상태는 프로토콜상 생기지 않는다(3단계가 4단계에 선행). → 의사결정 일지 D-043(오너 확인 대기 — 결정 자체는 여기 있다).

실행 순서 — 같은 앱 시작 훅(WorkspaceRecovery.runOnStartup). ① 쓰기 저널 복구(FileWorkspaceGate.reconcileWriteJournals) → ② 롤백 저널 재수행(§4.3 7단계, RollbackEngine.replayPendingJournals) → ③ edit_locks 리스 무효화(DD-13 트리거 3 — 잠금 구현 #37 이 붙인다). ①이 ②보다 먼저인 이유: 롤백 사전검사(DD-11 2단계)는 「그 경로의 마지막 스냅샷의 result」와 현재 해시를 대조하는데, 확정되지 않은 편집이 남아 있으면 마지막 스냅샷이 빠져 있어 그 대조가 틀린 답을 낸다. 두 단계 모두 게이트의 직렬 구간에서 돈다. 게이트는 첫 연산 전에 스스로 ①을 돌리고 남은 롤백 저널의 배타 구간을 세우므로, 이 훅이 늦게 불리거나 아예 안 불려도 그 사이의 에이전트 쓰기가 복구를 오염시키지 않는다 — 훅의 역할은 보고와 ②·③이다. 미해결(unresolved)·손상 저널의 경로는 게이트가 FileEditConflict.recoveryPending 으로 에이전트 쓰기를 막으며, 사람이 abandonWriteJournal(op_id) 로 그 상태를 받아들이면(레코드 없이 현재 내용 유지 — 사람의 편집은 손상이 아니라 충돌, 불변식 2) 풀린다. 게이트 안에서 4~5단계가 실패(예외)하면 같은 프로세스에서 곧바로 ①을 다시 돌려 C-02 를 세운다. 테스트가 이 순서를 단언한다. 복구는 멱등이다 — 같은 워크스페이스에서 연속 2회 수행해도 파일 해시·snapshots.jsonl 줄 수·blob 개수가 같다.

정합성 검사기(HistoryIntegrityChecker, 읽기 전용). .cocode/history 를 사후 스캔해 ① 한쪽 인덱스에만 있는 레코드 ② 참조하지만 objects/ 에 없는 blob ③ 현재 파일 해시 ≠ 그 경로의 마지막 result 인 경로(이름변경의 이전 경로는 기대 상태가 부재)를 건수로 보고한다. ③은 위반이 아니라 참고 지표다 — 사람의 직접 편집으로 정상 발생하며 스펙은 그것을 손상이 아니라 롤백 시 충돌로 제시하라고 규정한다(seed-spec 불변식 2). 남은 두 저널(해석 불가 포함)도 함께 보고한다.

CI. dart:io 쓰기 허용 파일 목록 가드(DD-15, #22)는 ci.yml changes 잡에서 위반 0건을 보고한다. C-01 의 데스크톱 3종 각각에서 AC 1·AC 2 를 실행하는 것은 .github/workflows/verify.cocode-pure-tests.yaml(ubuntu · macOS · windows-2022 매트릭스, 워크스페이스 밖 스테이징 — stage_cocode_pure_packages.py)이다. Windows 축은 D-040(#27)으로 열렸다 — File.rename 그대로.

4.4 보존 정책 (CH-022 해소 — DD-12)

Contrarian CH-022 는 "스냅샷 볼륨 무한 증가(보존·정리 정책 부재)"를 Major·Accepted(위험 인수) 로 처분하고 재검토 시점을 "Design 단계 저장소 설계"로 남겼다(contrarian 144·174행). 아래가 그 해소다.

참조값을 그대로 쓸 수 없는 이유 — 단위가 다르다.

Lumide 선례가 참고 기준으로 남아 있으나(contrarian 144행), 그 값은 파일당 상한이고 우리 롤백 단위는 작업(DelegatedTask) 이다. C-02 는 "그 시점 이후 같은 작업이 만든 편집은 함께 되돌아간다"를 요구하므로, 한 작업의 중간 버전만 지우면 그 작업의 원자적 롤백이 성립하지 않는다. 즉 파일당 버전 상한은 우리 롤백 의미론과 단위부터 어긋나며, 그것만으로 채택 불가 판정에 충분하다.

⚠️ rev.2 에서 삭제한 논증. 초판은 planning-inputs §3.2 의 ".dart blob 바이트를 ~38 로 나눈 추정" 비율을 역적용해 "10만 줄 ≈ 3.8MB → 파일당 5MB 상한은 버전 한두 개에서 소진"이라고 적었다. 이 논증은 셋 다 틀렸다: ⓐ ~38 은 저장소 전체 평균이지 단일 파일 특성이 아니며 같은 줄이 "명세에 적기 전 실제 체크아웃에서 검증할 것"을 요구한다 ⓑ PRD 872행이 "AC-15 픽스처 공백 — 10만 줄 파일을 어디서 얻는지 정해져 있지 않다"고 확인했으므로 존재하지 않는 픽스처의 바이트를 계산한 것이다 ⓒ §4.1 이 blob 을 gzip + dedup 으로 저장하기로 했으므로 원본 바이트 비교 자체가 성립하지 않는다. 그리고 PRD 718행 R-4 는 Contrarian 보고서의 수치를 "목표치로도 참조점으로도" 재인용하지 말 것을 규정한다 — 초판은 R-4 를 인용하면서 R-4 를 어겼다. 단위 불일치 논증만으로 결론은 그대로 서므로 수치를 쓰지 않는다.

정책 구조 — 값 없이도 확정되는 부분:

규칙 내용 판정 필요 여부
P1 종결 상태(완료·실패·롤백됨 — seed-spec 30행)에 도달하지 않은 작업의 스냅샷은 삭제하지 않는다 없음(상태값 비교)
P2 삭제 단위는 작업(DelegatedTask) 이다. 버전 단위로 지우지 않는다. 단위의 내용물(#32, DD-23c-④): 그 작업의 EditSnapshot 집합 전부 — tasks/<task_id>/snapshots.jsonl · files/<path_key>/versions.jsonl 의 해당 항목 · 그것들만 참조하던 blob(P5). 작업 레코드와 그 안의 verification_log 는 삭제 단위에 포함되지 않는다 — 롤백 재료가 아니고, 개수·exit code 뿐이라 부피도 유출 표면도 없다 없음
P3 고정(pin) — 어떤 파일 경로에 대해서든 그 경로의 최근 K개 작업 안에 드는 작업은 고정되며 P4 의 통상 삭제 대상에서 제외된다 없음(개수 비교)
P4 저장소 총량이 상한을 넘으면 고정되지 않은 작업 중 가장 오래 종결된 것부터 통째로 삭제한다. "통째로"의 범위는 P2 가 정한 단위 그대로다(#32, DD-23c-④) — 그 작업의 스냅샷 인덱스·버전 체인 항목·blob 은 전부 사라지고, verification_log 와 .cocode/history/tasks/<task_id>/ 의 작업 레코드는 남는다(레코드에 "스냅샷 정리됨" 사실이 남아 P6 의 표시 근거가 된다 — 필드·형식은 #23·#29 소유). 삭제 뒤에도 작업은 목록에 남으므로 ux-spec §4.5 R3 의 "[롤백] 비활성 + 사유" 를 그릴 대상이 있다 없음
P4′ 우선순위 규칙(rev.2 신설) — 고정되지 않은 작업을 전부 지워도 총량 상한을 넘으면, P4 가 P3 을 이긴다: 고정된 작업 중 가장 오래 종결된 것부터 통째로 삭제하고, 그 사실을 P6 으로 알린다. 이 조항이 없으면 모든 작업이 고정돼 상한이 영구히 집행되지 않는다 없음(집합 공집합 여부 + 시각 비교)
P5 삭제는 blob 참조 카운트가 0인 것만 회수한다(dedup 안전) 없음
P6 삭제 사실은 UI 에 "히스토리 정리됨"으로 표시하며, P4′ 로 삭제된 경우 "보존 한도를 넘어 오래된 롤백 지점을 정리함"을 구분해 표시한다 — 롤백 지점이 조용히 사라지지 않는다. 고지 종류의 경계(#32, DD-23c-④): P6 의 두 종류는 롤백 지점 소실만 다룬다. 검증 기록(verification_log 요약)은 P4 로 사라지지 않으므로 P6 의 고지 대상이 아니고, 검증 원문의 소실은 P4 사건이 아니라 세션 종료·세션 버퍼 정리 사건이라 P6 문구에 섞지 않고 별도 문구(DD-23c-② 의 고지 B·C — ux-spec §3.4·§4.4·§5.2)로 표시한다 — 사용자가 "롤백 지점이 없어졌다"와 "원문을 더 볼 수 없다"를 같은 문장에서 읽지 않게 한다 없음. AC-19 의 "조용히 …되지 않는다"와 같은 방향
P7 GC 트리거는 셋뿐이다 — 작업이 종결 상태에 도달한 직후 / 앱 시작 시 / 총량 상한 초과 직후 없음

⚠️ 초판의 P3 은 "파일 경로별로 최근 K개 작업의 스냅샷을 보존한다"였고 P2/P4 와 함께 "판정 필요 없음"으로 표기했는데, 한 작업이 파일 A(최근 K개 안)와 파일 B(밖)를 함께 건드리면 삭제 여부가 정해지지 않았다. P3 을 고정 규칙으로 바꾸고 P4′로 우선순위를 못박아 해소했다.

왜 버전 단위가 아니라 작업 단위인가: 롤백 단위가 작업이기 때문이다(C-02). 한 작업의 중간 버전만 지우면 그 작업의 원자적 롤백이 성립하지 않는다.

값 3개(K · 총량 상한 · 보존 기간)는 여기서 정하지 않는다. 근거가 없기 때문이다. 대신 요건과 절차를 고정한다:

  • 요건: 설치 직후 사용자 설정 없이도 세 값이 양의 유한한 기본값을 가져야 한다. (AC-08 이 시간 상한에 건 요건과 같은 형태를 차용한 것이며, Seed Spec 이 보존 정책에 이 요건을 건 것은 아니다 — 이 문서의 요구다. §10-⑤ 참조.)
  • 확정 주체·시점: Planning, A-06 의 "최소 셸 조립 후 실측" 스파이크와 같은 회차에서 참조 워크스페이스(coco-de/unibook, D-019, 커밋 SHA 고정 필수 — planning-inputs §3.2) 기준으로 실측한 뒤. 근거는 D-020 의 원칙 — "실측 분포를 보기 전에 숫자를 정하면 같은 패턴이다."
  • 측정해야 할 것: 작업 1건당 스냅샷 수 · gzip·dedup 후 blob 총 바이트 · 파일 크기 분포 · 파일당 편집 작업 수 분포. 측정치는 PRD R-1(718행 위)이 요구하는 7개 필드(커밋 SHA·디렉터리·파일 경로·빌드 모드·하드웨어 등급·플랫폼·표본 수·수집 도구·수집 일시)를 전부 동반해야 인용 가능하다.
  • 재도입 금지: Contrarian 보고서에 남은 어떤 수치도 목표치로도 참조점으로도 옮겨 적지 않는다(PRD R-4).
  • 산정 규칙·측정 규약·후보 (#30 → D-039): docs/retention-values-cocode.md. 값은 규칙(V-1~V-4) · R-1 표본 · 재산정 조건 3요소로만 도출하며 사는 자리는 planning-inputs §7 이다(값 확정은 #29). ⚠️ 위 "값 3개" 중 보존 기간은 P1~P7 어디에서도 읽히지 않는다 — #30 이 이를 확인해 MVP 값 집합에서 제외했다. 시간 기반 정리를 원하면 규칙 P8 신설이 값에 선행한다(같은 문서 §3 V-3). 이 표의 P 행은 이 문서가 고치지 않았다(P4·P6 본문은 #32 소유).

📌 구현 주석 (#29, 2026-09-12) — workspace lib/src/retention/ 의 RetentionGc 가 P1~P7 과 P4′ 를 수행한다: 삭제 단위는 작업(SnapshotChainStore.removeTaskRecords — 작업 인덱스 삭제 + 경로 체인에서 그 작업 줄만 빼고 임시 파일·rename 으로 재작성, 빈 체인은 디렉터리째 제거) · P3 고정은 어느 경로 체인에서든 최근 K개 작업 안이면 고정 · P4 는 고정되지 않은 종결 작업을 오래 종결된 순으로, P4′ 는 고정된 것도 같은 순으로 — 단 E-1 바닥 규칙으로 가장 최근 종결 작업 1건은 남기고 초과 상태를 보고한다(RetentionGcReport.overCapacity, E-2 의 「P1 보호만으로 초과」도 같은 표시) · P5 는 살아 있는 레코드와 남은 쓰기·롤백 저널이 참조하는 blob 을 제외하고 회수 · P6 은 tasks/<task_id>/retention.json 마커(종류 capExceeded/pinnedPruned · 트리거 · 시각 · 사라진 스냅샷 수)로 남기고 RollbackAvailability 가 [롤백] 비활성 + 사유의 데이터 계약이다(UX-D-12 R3) · P7 의 진입점은 onTaskTerminated·onAppStart·onCapExceeded 셋뿐이다. GC 1회는 게이트의 직렬 구간(FileWorkspaceGate.runSerially)에서 돈다 — 게이트가 blob 을 적재하고 레코드를 append 하는 사이에 끼어들면 참조되지 않은 blob 을 회수하거나 체인 재작성으로 레코드를 잃는다. 값 2종(K · 총량 상한)은 RetentionSettings(출하 기본 K=14 · 80 MiB — 근거·표본·재산정 조건은 planning-inputs §7.2, D-044) 한 곳에만 있고 .cocode/config/workspace.json 의 history.retention 으로 덮어쓴다. 작업 종결 여부는 조회 seam(TaskTerminationLookup)으로 받는다 — 작업 레코드는 이 패키지 밖(셸 Epic)이고, 모르는 작업은 종결 아님으로 취급한다(P1 쪽이 안전측).

4.5 잠금 (AC-12 / AC-18) — DD-13

edit_locks 는 파일 경로 → 리스(소유 task_id, 소유 세션 식별자, 갱신 시각)로 둔다.

  • 잠금은 에이전트가 그 파일의 편집을 마치는 시점에 해제되고, 승인 대기 중에는 유지하지 않는다(Workspace 불변식 2). "편집을 마치는 시점"의 판정 기준은 확정됐다(D-045, #76 — docs/edit-lock-lifecycle-cocode.md): 리스 구간은 [게이트 연산 호출 직전, 그 연산의 반환 직후] 이며 획득은 편집 시도 시점(작업 시작 시 일괄 예약이 아니다), 해제는 그 게이트 연산의 반환(PRD D-14 후보 ③ 「도구 호출 반환」의 이 아키텍처에서의 좌표)이다. 좌표를 「요청 종결」이 아니라 「게이트 연산 반환」으로 잡은 이유는 C-05 승인 대기가 FileEditRequested 요청 처리 구간 안에 들어오기 때문이다 — 요청 단위로 잡으면 승인 대기 내내 리스를 들게 되어 불변식 2 후반부와 어긋난다. 이 순서로 후반부는 런타임 검사 없이 구조적으로 성립한다. 실패·거부 경로도 같은 경계에서 해제한다(구간의 종료이지 정상 반환이 아니다).
  • AC-18(비정상 종료 시 잠금 해제) — rev.2 정정. 초판은 "소유 프로세스가 존재하지 않으면 리스는 무효"만 적었으나 그것으로는 두 백엔드 모두 커버되지 않는다. agent 는 인프로세스라 죽을 별도 프로세스가 없고, acp 자식이 죽어도 cocode ADE 프로세스는 살아 있어 리스가 유효로 남는다. 해제 트리거를 셋으로 둔다:
    1. 세션 종료 트리거 — AgentSession 이 TransportClosed 를 내거나 dispose() 되면, 그 세션 식별자가 소유한 모든 리스를 즉시 해제한다. 이것이 acp 자식 사망과 agent 스트림 사망을 모두 덮는다(판정 없음 — 사건 발생 여부).
    2. 작업 종결 트리거 — DelegatedTask 가 종결 상태에 도달하면 그 task_id 의 리스를 전부 해제한다.
    3. 앱 재기동 트리거 — 앱 시작 시 남아 있는 리스는 전부 무효다(아래). Seed Spec AC-18 이 요구하는 것은 "잠금이 해제되어 있고 다른 작업이 그 파일을 편집할 수 있다"이며, 세 트리거 어느 것도 "비정상 종료"를 분류하지 않으므로 PRD D-13(비정상 종료 감지 기준, §9 미결 8)이 미결인 채로도 AC-18 은 성립한다.
  • edit_locks 의 초기값은 빈 집합으로 정한다(PRD D-17 해소). 근거: 리스 소유자인 AgentSession 은 cocode ADE 프로세스 수명에 종속되므로, 앱 시작 시 유효한 세션이 존재할 수 없다. 시작 시 잔존 리스는 트리거 3으로 일괄 폐기한다.
  • 미결 유지: 비정상 종료한 작업 자신의 status 는 Seed Spec 이 규정하지 않는다(PRD D-13·§4.1). 이 문서는 "실패로 전이"를 제안하되 확정하지 않는다 — 근거가 연역이 아니라 유추이기 때문이다(§9 미결 8).

📌 구현 주석 (#75, 2026-09-12) — workspace lib/src/lock/ 의 FileEditLockRegistry 가 DD-13 을 구현한다. 계약 표면은 core 의 EditLockRegistry(#18 이 이름만 두었던 자리)이고, 배선 지점은 ApplyFileEditUseCase(게이트 연산을 리스 구간으로 감싼다 — D-045 §2)와 WorkspaceRecovery.runOnStartup 3단계(트리거 3)다. 확정된 것 넷:

  • 구간 API 하나만 연다(withLeases(paths, context, action)). 자유 put/remove 는 짝을 호출자가 기억하게 만들고, 한 번 빠뜨린 경로가 곧 유령 잠금이다. 예외·거부도 같은 경계에서 해제한다(D-045 §2.2).

  • 잠그는 경로는 FileEditOperation.touchedPaths — #22 가 그 게터의 문서에 「잠금(DD-13)은 이 목록의 경로 전부에 걸린다」고 이미 규정했다. 획득은 전부-아니면-전무다 — 하나라도 막히면 하나도 잡지 않고 엄격 FIFO 로 기다린다. ⚠️ D-046 초판의 「정렬해 하나씩 획득」은 포함 충돌에서 성립하지 않는다(구현 리뷰가 교착 3종을 재현했고, 추월 예외로 막으면 기아가 재현됐다 — edit-lock-queue-cocode.md §3 정정 상자). 전부-아니면-전무는 「리스를 든 채 기다리는 상태」를 없애 교착과 기아를 한 성질로 닫는다. 정렬은 남되 역할이 아래 중첩 키 제거와 결정성이다.

  • ⚠️ 충돌 판정은 정확 일치가 아니라 경로 포함 관계다. 디렉터리 연산의 touchedPaths 는 디렉터리 경로 하나인데 게이트는 그 하위 정규 파일 전수를 지운다(§4.7 R5-3 · _deleteDirectory). 정확 일치만 보면 delete('lib/foo') 가 도는 동안 다른 작업의 write('lib/foo/bar.dart') 가 통과해 같은 파일을 두 작업이 동시에 편집한다 — 불변식 2 전반부 위반이다. 그래서 두 키가 같거나 한쪽이 다른 쪽의 하위 경로면 충돌로 본다. 확장 집합을 미리 알 수 없는 층(L0 유즈케이스)에서도 성립하는 유일한 표현이다. ⓐ :504 의 서술이 이보다 좁다(「rename 은 from·to 두 경로」) — 다중 경로는 rename 만이 아니므로 문면 정정 대상이다.

  • locks/edit_locks.json 은 쓰되 시작 시 항상 버린다. 재기동 후 유효한 리스로 살아나는 일이 없는데도 쓰는 이유는 둘이다 — 파일이 없으면 트리거 3 이 검증할 대상 없는 문장이 되고(#78 이 그것을 검증한다), 죽는 순간 무엇이 잠겨 있었는지가 그 파일 말고 남는 곳이 없다. 파일은 완전한 리스 집합만 담는다(획득 도중의 부분 상태는 쓰지 않는다 — 시작 시 폐기되므로 그 정밀도에 correctness 역할이 없다).

  • 표시 순서 (D-048, #78) — S-01b 의 잠금 칸은 EditLockRegistry.startupSweepCompleted 가 true 가 된 뒤에만 렌더한다. 스윕 전에는 0 도 그리지 않는다: 0 은 사실 주장인데 값이 확정되지 않았고, 그 시점의 실제 크기를 그리면 유령 잠금 수가 화면에 뜬다(AC-18 이 없애려는 상태가 사용자에게 도달). 「첫 프레임」은 렌더 계층 사건이라 이 패키지가 판정하지 못하므로 #78 은 데이터 계약까지 단언하고 실기기 검증은 셸 Epic 에 남긴다.

  • ⚠️ 트리거 1·2 는 리스를 회수할 뿐 진행 중인 구간을 멈추지 못한다 — 그 공백과 소유(#71)는 §9 미결 22. 코드 리뷰가 실측으로 재현했다.

«개정 요청» (오너 판정 — 의사결정 일지 D-047) — seed-spec:70(§3 Workspace Attributes)의 edit_locks 정의 「에이전트가 현재 편집 중인 파일 경로 집합」을 경로 → 리스 사상으로 고친다. §3 머리말이 「잠금 후 기존 엔티티 수정 불가, 추가만 허용」이므로 이것은 구현 재량이 아니라 문서 결정이다(DD-25 → D-038 과 같은 경로). 판정 전까지의 상태는 §10-20 에 어긋남으로 적는다.

4.6 스캐폴딩 원자성 (AC-13) — DD-13a (rev.2 신설)

초판은 bricks 의 역할에 "스캐폴딩 원자성(AC-13)"을 적고 의존 대상에 workspace 를 넣었으나, 새 워크스페이스를 만드는 시점에는 워크스페이스도 .cocode/ 도 게이트도 존재하지 않으므로 게이트가 그 원자성을 매개할 수 없다. 기제를 명시한다.

  1. 스테이징 — package:mason 을 인프로세스로 링크하되(Discovery 111행: mason_cli 셸아웃 금지), 생성기 출력을 커스텀 GeneratorTarget 으로 받아 최종 위치가 아닌 같은 볼륨의 스테이징 디렉터리에 쓴다. 이것이 DD-15 의 "workspace 밖 dart:io 쓰기 금지"를 만족시키는 유일한 경로다.
  2. 훅 실행 — brick 훅은 toolchain 런처를 경유하고 cwd 를 스테이징 디렉터리로 고정한다. 훅이 스테이징 밖을 쓰면 런처가 차단한다(§5.4).
  3. 커밋 — 전량 성공 시 스테이징을 최종 위치로 단일 디렉터리 이동한다(같은 볼륨). 실패 시 스테이징 디렉터리 전체를 삭제하며, 최종 위치에는 아무것도 만들어지지 않는다 → Workspace 불변식 3 · AC-13 의 "부분 생성된 파일은 전량 제거되고 워크스페이스는 생성되지 않은 상태로 남는다".
  4. "진행 중 실패"의 판정 기준 — D-049 로 확정(#41). 원인이 아니라 위치로 가른다: 반열린 구간 [첫 스테이징 쓰기, 커밋 완료) 안에서 일어난 모든 비정상 종료가 진행 중 실패이며 3번의 전량 삭제를 발동한다. 첫 쓰기 이전의 종료는 착수 전 거부로, 스테이징이 없으므로 지울 것도 워크스페이스도 없다(불변식 자명 성립). 커밋 완료 이후는 스캐폴딩이 아니라 워크스페이스 연산이며 게이트를 경유한다. 커밋(단일 디렉터리 이동) 자체의 실패는 구간 안이다 — 이동이 원자적이라 최종 위치에는 아무것도 없다. PRD 후보 4종(훅 비정상 종료 / 쓰기 오류 / 변수 검증 실패 / 사용자 취소)과 2번의 「훅이 스테이징 밖을 쓰려다 차단됨」은 열거가 아니라 이 구간에 매핑된다 — 변수 검증 실패와 사용자 취소는 시점에 따라 갈린다. 따라서 BrickEngine 은 「첫 스테이징 쓰기를 했는가」를 상태로 들고 있어야 한다(#40). 사용자 제시 형태는 ux-spec UX-D-23.

co-brick 적용은 MVP 범위 밖이다 — D-037(#113, 프로젝트 오너 확정) · Seed Spec AC-24(Must-Not). 따라서 이 절이 규정하는 원자성은 워크스페이스 최초 스캐폴딩 한 경로에만 적용되고, applied_bricks 는 MVP 에서 원소가 1개다. (기존 워크스페이스에 co-brick 을 덧붙이는 경로가 MVP 이후 도입되면 워크스페이스가 이미 존재하므로 게이트를 경유한다 — 그때는 이 절의 스테이징·커밋 기제가 아니라 §5.4 의 쓰기 경로 집행을 받는다.)

📌 구현 주석 (#40, 2026-09-12) — DD-13a 스캐폴딩 원자성 — bricks(L2 신설)의 ScaffoldEngine 이 1~3 을 수행한다.

스테이징은 최종 위치의 형제다 — <parent>/.cocode-staging-<name>-<stamp>. Directory.systemTemp 를 쓰지 않는 이유가 3번의 "같은 볼륨"이다: 다른 파일시스템이면 rename 이 복사+삭제로 떨어져 중간 상태가 관찰 가능해지고 원자성이 사라진다. 커밋은 staging.rename(destination) 한 줄이다.

mason 의 훅 실행기를 쓰지 않는다. MasonGenerator.hooks 는 자기가 프로세스를 띄우므로 훅이 우리 런처를 지나지 않아 환경변수 화이트리스트(DD-23a)도 호스트 집행(DD-16)도 받지 않는다. 그래서 mason 은 파일 생성에만 쓰고(인프로세스 링크 — Discovery 111행의 mason_cli 셸아웃 금지를 만족) 훅은 ProcessLauncher 로 직접 실행하며 cwd 를 스테이징으로 고정한다(C6). 이것이 #40 인수조건 4·5 를 동시에 만족하는 유일한 조합이다.

탈출 차단은 정규화 후 판정이다. StagingGeneratorTarget 은 p.normalize 로 .. 를 접은 뒤 스테이징 루트 안인지 본다 — 접기 전에 검사하면 a/../../x 가 통과한다. 절대 경로 입력도 같은 검사를 받는다(brick 이 /etc/x 를 내는 것도 탈출이다).

결과 타입이 D-049 의 구간을 그대로 든다 — ScaffoldSucceeded / ScaffoldRejectedBeforeStart (첫 쓰기 이전) / ScaffoldFailedInProgress([첫 쓰기, 커밋 완료)). sealed 로 둬서 호출부가 둘을 구분하도록 강제한다: UX-D-23 이 그 둘에 다른 화면을 주기 때문이다(전자는 폼 필드 오류, 후자는 「아무것도 만들어지지 않았습니다」). 삭제가 실패하면 stagingRemoved: false 로 보고하고 조용히 성공으로 접지 않는다.

⚠️ 의존성 함정: mason 이 끌어오는 mason_logger 의 최신 0.3.5 는 win32 ^5.11.0 으로 회귀했는데 이 리포는 serverpod 정합으로 win32 를 6.x 로 override 한다. 그러면 ffi/windows_terminal.dart 가 kernel 컴파일에서 깨지며 macOS 에서도 깨진다 — 루트 pubspec 의 win32 override 주석이 적어 둔 「mason_logger 는 dev 전용이라 안전」이라는 전제가 이 Story 로 무너졌다. ^6.0.0 을 선언하는 유일한 판본인 0.3.4 에 고정했고, 워크스페이스 밖 3-OS 사본에는 stage_cocode_pure_packages.py 가 같은 override 를 다시 심는다(사본은 루트 override 를 물려받지 못해 CI 에서만 깨지는 상태가 된다).

4.7 파일 생성·삭제·이름변경의 스냅샷 표현과 롤백 (#28) — DD-25

문제. Seed Spec §3 은 base_content_hash 를 "편집 직전 해당 파일 내용의 해시", result_content_hash 를 "편집 직후 해당 파일 내용의 해시" 로만 정의한다. 그래서 생성은 base 가, 삭제는 result 가 정의되지 않고, 이름변경은 file_path 하나에 담기지 않는다(flow-permutation §6.2 I-4 · T-C FP-306·FP-307). AC-11 이 "새 테스트 파일 생성"을 인정하므로 생성 경로는 실재하고, 규칙이 없으면 #23(저장소)·#25(롤백 저널)가 각자 다른 표현을 만든다. 아래 R1~R7 이 그 규칙이며 세 연산 모두 규칙이 섰다 — 미정으로 남긴 축은 절 끝의 "정하지 않는 것" 하나뿐이고, 그것은 세 연산의 표현이 아니라 게이트 밖 경로다. 전제는 v3.3.0 의 D-035(보장 범위 = 보존 정책 유지분)·D-036(충돌 판정의 작업 범위 한정)이다.

R1 — 부재값(∅). 두 해시 속성의 값 집합을 {내용 해시} ∪ {∅} 로 확장한다. ∅ 는 "그 시점에 그 경로에 파일이 없음"을 뜻하는 구별값이며 빈 파일의 해시(sha256("") = e3b0c442…b855)와 다르다 — 빈 파일은 존재하는 파일이다. 규칙 둘이 따라온다: ⓐ "현재 내용 해시"는 경로에 정규 파일이 없으면 ∅ 이고, 정규 파일이 아닌 것(디렉터리·심볼릭 링크)이 있으면 ∅ 도 해시도 아니므로 어떤 값과도 일치하지 않는다(→ 충돌) ⓑ ∅ 로의 복원은 파일 삭제다. 저장 인코딩(권고: JSON null)은 #23 이 정하되 ∅ 와 빈 파일 해시를 반드시 구분한다.

R2 — 종류는 저장하지 않고 파생한다. 스냅샷의 종류는 (base, result, previous_path) 세 값이 유일하게 결정한다. 별도 kind 속성을 두면 세 값과 어긋날 수 있는 네 번째 값이 생기므로 두지 않는다(엔티티의 파생 getter 는 무방하다).

종류 base result previous_path 존재 조건
수정 H1 H2 없음 현행 그대로
생성 ∅ H 없음 편집 직전 그 경로에 파일 없음
삭제 H ∅ 없음 편집 직전 그 경로에 파일 있음
이름변경 H H (= base) 있음 내용 보존. 이름 바꾸기와 디렉터리 간 이동을 구분하지 않는다
(금지) ∅ ∅ — 존재하지 않는다 — 게이트가 거부한다(R5)
(금지) H1 H2 ≠ H1 있음 존재하지 않는다 — 이름변경은 내용을 바꾸지 않는다. 내용 변경은 그 뒤의 별도 수정 스냅샷이다

R3 — 이름변경은 스냅샷 1건이고 경로 2개로 투영된다. 속성 previous_path(이름변경 직전 경로)를 신설한다. 하나의 이름변경 스냅샷은 previous_path 에 (H → ∅), file_path 에 (∅ → H) 두 투영 전이를 낳는다. 수정·생성·삭제는 file_path 에 투영 전이 1개를 낳는다. sequence_no 는 스냅샷당 하나다(이름변경도 편집 1건).

왜 삭제+생성 2건이 아닌가. 두 스냅샷으로 표현하면 둘 사이에 "특정 스냅샷으로 롤백" 경계가 놓일 수 있고, 그 경계로 되돌리면 파일이 두 경로 모두에 없는 상태가 된다 — 스냅샷 경계는 사용자가 되돌릴 수 있는 시점인데(C-02) 그 시점은 에이전트가 실제로 거친 상태가 아니다. 두 건을 떼어낼 수 없게 묶는 속성을 두느니 한 건으로 두고 투영하는 쪽이 속성도 판정도 적다. 별도 kind 속성을 두는 안은 R2 에서 같은 이유로 버렸다.

R4 — 불변식 2·4 와 DD-11 사전검사는 투영 전이 단위로 적용한다. 롤백 대상은 (경로, 투영 전이) 쌍의 집합이다. 각 경로에 대해:

  • 복원 목표 = 롤백 집합 안에서 그 경로의 최소 sequence_no 투영 전이의 base 상태(불변식 4 그대로 — ∅ 이면 삭제, H 이면 blob H 로 복원).
  • 충돌 ⓑ = 현재 상태 ≠ 롤백 대상 작업이 그 경로에 남긴 마지막 투영 전이의 result 상태(불변식 2 · D-036). 충돌 ⓐ(그 경로의 전역 마지막 스냅샷이 대상 작업 것인가)는 경로별로 그대로 본다 — 이름변경은 두 경로 모두.
  • 판정을 넣지 않는다. 사람이 이미 목표 상태로 되돌려 둔 경우(에이전트가 만든 파일을 사람이 먼저 지운 뒤 롤백 = 현재 ∅ ≠ result H)도 충돌이다 — FP-305 후단("에이전트 편집 이전 원본으로 되돌린 경우는 충돌")과 같은 판정이며, "결과적으로 같으니 통과"를 계산하기 시작하면 불변식 2 의 "자동 적용하지 않는다"가 판정 규칙으로 변한다.
종류 작업 시작 직전 롤백이 하는 일 충돌 ⓑ 가 성립하는 현재 상태
생성 (∅→H) file_path 삭제 ≠ H — 사람이 고쳤거나(H′) 지웠거나(∅)
삭제 (H→∅) blob H 로 file_path 재생성 ≠ ∅ — 그 경로에 파일이 다시 있음(내용 무관)
이름변경 (prev: H→∅ · path: ∅→H) file_path 삭제 + previous_path 를 blob H 로 재생성 previous_path 에 파일이 있음, 또는 file_path ≠ H
수정 (H1→H2) 현행 현행

같은 작업 안의 연쇄는 투영으로 자연히 접힌다: 생성 후 같은 작업이 삭제(∅→H→∅)하면 시작 직전 복원 목표는 ∅ 이고 현재 ∅ 이면 충돌 없이 아무 일도 하지 않는다(멱등 — DD-11 7단계와 같은 규칙). 삭제 후 재생성(H1→∅→H2)의 복원 목표는 H1 이다. 생성 후 이름변경(a: ∅→H→∅ · b: ∅→H)은 a·b 모두 ∅ 로 돌아간다. #89 가 정할 "선택한 스냅샷 자신의 포함 여부"는 종류와 무관하므로 이 절의 영향을 받지 않는다.

R5 — 게이트 연산 어휘와 거부. WorkspaceFileGate(#22) 의 편집 연산은 셋이다 — write(path, bytes) · delete(path) · rename(from, to). 생성/수정의 구분은 에이전트가 고르지 않고 쓰기 직전 경로의 존재 여부가 정한다(ACP 의 fs/write_text_file 도 이 규칙으로 생성이 된다). 다음은 스냅샷을 남기지 않고 거부(에이전트에 오류 반환)한다 — 각각 R2 의 금지 행이거나 투영이 성립하지 않는 경우다:

  1. delete 의 대상, rename 의 from 이 ∅ — 부재→부재.
  2. rename 의 to 가 ∅ 가 아님(덮어쓰기 — 두 파일의 운명이 한 스냅샷에 섞인다. 먼저 delete(to) 를 내면 삭제 스냅샷이 따로 남는다), 또는 from == to.
  3. 대상 경로에 정규 파일이 아닌 것(심볼릭 링크 등)이 있는 write/delete/rename — 그 상태는 ∅ 도 해시도 아니라 스냅샷이 성립하지 않는다. 단 디렉터리를 대상으로 한 delete/rename 은 거부하지 않고 파일 단위로 분해해 같은 attempt_no 의 연속 sequence_no 스냅샷 N건으로 남긴다. 분해된 N건 중 하나라도 1·2 에 걸리면 연산 전체를 거부한다(부분 적용 없음). 스냅샷의 주어는 파일이며 디렉터리(특히 빈 디렉터리)는 추적하지 않는다 — 따라서 롤백은 빈 디렉터리를 만들거나 지우지 않는다(git 과 같은 의미론).

그 밖의 게이트 의무: ⓐ delete 는 unlink 전에 현재 내용을 blob 으로 적재한다(DD-10 "쓰기 전에 원본을 blob 으로" 의 삭제판 — 이것이 없으면 삭제의 복원 원본이 없다) ⓑ rename 은 from·to 두 경로 모두에 edit_locks 리스(AC-12)와 허용 경로 판정(#37)을 요구하고, 롤백의 rollback_in_progress 배타 구간(§4.3 8단계)도 두 경로에 걸린다.

R6 — C-05/AC-11 의 테스트 보호는 투영 전이 단위로 판정한다. 테스트 경로 집합은 현행(test/ · integration_test/ · widgetbook/)이다. 테스트 경로에 대한 투영 전이가 (H→∅) 또는 (H1→H2) 이면 그 편집은 "기존 테스트 파일에 대한 변경"이라 pending_approval_kind = 테스트변경승인 대상이고, (∅→H) 만이 예외(새 테스트 파일 생성)다. 귀결: 테스트 파일 삭제 → 승인 · 테스트 경로 안에서의 이름변경 → 승인(이전 경로의 H→∅) · 테스트 경로 밖으로의 이동 → 승인 · 밖에서 테스트 경로 안으로의 이동 → 승인 불요(테스트 경로 쪽 전이가 생성이고 이전 경로는 테스트가 아니다). 근거는 C-05 본문의 "모든 변경"이다 — 삭제를 변경에서 빼면 "삭제 후 새로 생성"으로 C-05 전체가 우회된다. ⚠️ AC-11 문면은 "내용을 변경"이라 이보다 좁게 읽힌다 → 아래 개정 요청 ③, §10-17.

R7 — 저장 레코드·diff·대기 변경. DD-10 레코드에 선택 필드 prev(이름변경에만)를 추가하고 base/result 가 ∅ 를 표현할 수 있게 한다(권고: JSON null). blob 은 존재하는 상태에만 있다. diff 파생(DD-10)은 ∅ 를 빈 내용으로 두고 계산한다 — 생성은 전량 추가, 삭제는 전량 삭제, 이름변경은 hunk 없이 경로 변경 헤더만. 승인 대기 중 미적용 변경을 담는 PendingChange(ux-spec UX-D-19)도 같은 세 값 (base, result, previous_path) 어휘를 써야 R6 의 대기 삭제·이름변경을 담을 수 있다. 보존 정책 P5 의 blob 참조 카운트는 ∅ 를 참조로 세지 않을 뿐 그대로다.

📌 구현 주석 (#26, 2026-09-12) — 화면(#82·#83·#84)이 읽는 데이터 계약은 core 의 SnapshotStore 표면(snapshotsOfTask·lastSnapshotOfPath·diffBetween)과 유즈케이스 3종(ListTaskSnapshotsUseCase — 스냅샷 단위·작업 단위 두 축, 이름변경 사슬을 최종 경로 한 행으로 접고 배지는 (첫 base, 마지막 result) 두 값만으로 정한다(ux-spec §4.1 UX-D-21) · ComputeRollbackTargetsUseCase — #24 순수 함수 + 「스냅샷 N건 · 경로 M개」와 경로별 동작 미리보기(§4.2) · PrecheckRollbackUseCase — #24 순수 술어를 게이트 실측(WorkspaceFileGate.observe)으로 감싼 파일별 판정 + 집합 단위 canApply(§4.3, UX-D-09))이다. 어느 계약도 작업의 status 를 받지 않는다 — 실행 중 작업의 「지금까지」와 실패한 작업(AC-19)이 같은 조회다. diff 는 저장하지 않고(DD-10) workspace 의 WorkspaceSnapshotStore 가 조회 시 base/result blob 을 읽어 UnifiedDiff.compute(줄 단위 Myers · 문맥 3줄 · 편집 거리 2048 초과는 전량 치환)로 파생한다 — ∅ 는 빈 내용, 이름변경은 헤더만, NUL 을 포함한 내용은 이진 안내문(DD-25 R7). 사전검사의 판정은 제시용이며 적용 시점의 판정은 롤백 엔진이 직렬 구간 안에서 다시 한다(#25).

소유 경계 (이 결정을 받는 쪽).

이슈 이 결정에서 받는 것
#22 게이트 R5 연산 어휘·거부 3종·삭제 전 blob 적재·이름변경 2경로 잠금. FileEditRequested 가 세 연산을 구분해 실어야 한다
#23 저장소 R1 ∅ 인코딩, R7 prev 필드, 이름변경 1건이 files/<path_key>/versions.jsonl 두 체인 모두에 같은 snapshot_id 로 기록될 것(DD-11 ⓐ 가 두 경로에서 성립하려면 필수)
#24 순수 함수 스냅샷 레코드를 투영 전이 목록으로 펼치는 함수(스냅샷 1건 → 1~2건)와, 현재 상태 입력에 ∅·"정규 파일 아님"을 포함하는 것. 대상 집합은 (경로, 투영 전이) 쌍
#25 롤백 저널 목표 상태 ∅ = unlink, H = blob 치환. 이름변경 되돌림을 물리적 rename 으로 최적화해도 되나 최종 상태(R4 표)가 같아야 하고 저널 항목은 경로별로 남긴다
#31 쓰기 저널 크래시 주입 AC 의 ⓐ/ⓑ 상태쌍에 ∅ 를 포함해 삭제·이름변경(두 경로) 경계도 검사한다
#26 · #82 · #83 표시 규칙은 ux-spec §4.1~§4.3(UX-D-21)
#37 허용 경로 술어를 이름변경의 두 경로에 각각 적용
#89 영향 없음 — "선택한 스냅샷 자신의 포함 여부"는 종류와 무관하다

이 결정이 정하지 않는 것.

  • 게이트를 거치지 않은 삭제·이름변경의 처분 — 미정 — 결정 필요(판정 주체: Design, #22·#37 소유 경계). ACP v1 의 fs 능력은 fs/read_text_file·fs/write_text_file 뿐이라 외부 ACP 에이전트의 삭제·이름변경은 터미널 명령(rm·mv)으로만 일어나고, 그 경로는 게이트 밖이다. 이것은 §10-② 가 이미 기록한 C-02 구멍이며 이 결정이 넓히지 않는다 — 감시자가 검출한 스냅샷 없는 변경을 어떻게 표시·처분할지는 §9 미결 19 로 남긴다. agent 자체 도구는 세 연산을 게이트로 보내므로 이 구멍이 없다.
  • 파일 메타데이터(실행 비트·심볼릭 링크 대상)는 추적하지 않는다 — 내용 바이트만 다루는 현행 한계이며 새로 생긴 것이 아니다.

적용 순서. ✅ Seed Spec 문면 반영은 v3.4.0 으로 완료됐다(오너 확정 2026-09-10 · D-038). 엔티티 개정(∅ 표현 · previousPath · 투영 전이 getter)이 #23 착수의 선행이며 그 개정도 함께 수행됐다. 그렇지 않으면 D-036 이전의 #24 와 같은 "구현이 스펙보다 앞선" 상태가 다시 생긴다.

✅ «개정 요청» — Seed Spec v3.4.0 으로 채택됨 (프로젝트 오너 확정 2026-09-10 · D-038)

①~④ 가 그대로 docs/seed-spec-cocode.md v3.4.0 에 반영됐다. 아래는 그 제안 원문을 보존한 것이다 — 대안 ⓐ·ⓑ 는 기각·불가로 판정났다.

① §3 EditSnapshot · Attributes

  • base_content_hash / result_content_hash 의 값 집합에 부재(absent) 를 추가한다 — "편집 직전/직후 그 경로에 파일이 존재하지 않음을 나타내는 구별값. 빈 파일의 해시와 다르다. 부재로의 복원은 파일 삭제다."
  • 속성 신설 previous_path(선택 — 이름변경 스냅샷에만 존재. 이름변경 직전의 경로. 이름변경은 내용을 보존하므로 base_content_hash = result_content_hash 다).

② §3 EditSnapshot · Invariants 추가 (기존 불변식 1~5 는 그대로)

  • 불변식 6 — 스냅샷의 종류는 (base_content_hash, result_content_hash, previous_path) 로 유일하게 결정된다: 수정(해시→해시, previous_path 없음) · 생성(부재→해시) · 삭제(해시→부재) · 이름변경(previous_path 있음, base = result). 부재→부재인 스냅샷과 previous_path 가 있으면서 base ≠ result 인 스냅샷은 존재하지 않는다.
  • 불변식 7 — 불변식 2(충돌 판정)·4(작업 시작 직전 복원)·「시도 경계 롤백」의 "그 파일"은 경로별 투영 전이로 읽는다: 이름변경 스냅샷은 previous_path 에 (해시→부재), file_path 에 (부재→해시) 두 전이로 투영되고 나머지 종류는 file_path 에 한 전이로 투영된다. "현재 내용 해시"는 그 경로에 파일이 없으면 부재이며, 정규 파일이 아닌 것이 있으면 어떤 값과도 일치하지 않는다(충돌).

③ C-05 · AC-11 문면 명확화

  • AC-11 의 "기존 테스트 파일(…)의 내용을 변경하려 할 때" → "기존 테스트 파일에 대한 변경(내용 변경 · 삭제 · 이름변경 전 경로가 테스트 경로인 이름변경)을 적용하려 할 때". 예외 문장은 "테스트 경로에 대한 생성(부재→해시)만이 이 제한의 예외다"로.
  • C-05 본문은 이미 "모든 변경"이라 개정이 필요 없다. Rationale 에 "삭제·이름변경도 변경이다 — 아니면 삭제 후 재생성으로 우회된다" 한 줄 추가를 권고한다.
  • AC-01 · AC-02 문면은 그대로 두어도 불변식 7 로 읽히므로 개정 대상이 아니다.

④ §6 Evolution Log 행(초안)

| v3.4.0 | (판정일) | 파일 생성·삭제·이름변경의 스냅샷 표현 (#28). ①두 해시 속성에 부재값 도입 ②previous_path 신설 ③불변식 6·7(종류 유일 결정 · 경로별 투영) ④AC-11 문면 명확화(삭제·이름변경 포함, 생성만 예외) | I-4 / FP-306·307 해소. Design 결정은 architecture §4.7 DD-25 | 프로젝트 오너 |

대안(오너가 ①~④를 기각할 때 남는 선택지): ⓐ 이름변경을 삭제+생성 2건으로 표현하고 둘을 떼어낼 수 없게 묶는 속성을 신설 — 속성 1개가 늘고 R3 의 경계 문제를 묶음 규칙으로 막아야 한다 ⓑ 생성·삭제·이름변경을 EditSnapshot 대상에서 제외 — AC-11 이 생성을 인정하고 C-02 가 "EditSnapshot 없는 편집 경로는 없다"라 불가.


5. (e) 샌드박스 집행 — 프로세스 격리인가 인터셉트인가

5.1 인터셉트로는 집행할 수 없다는 논증

AC-14 ⓑ가 막아야 할 네트워크 요청의 발신 주체는 cocode ADE 의 Dart 코드가 아니다. planning-inputs §2.1 이 열거한 호스트에 접속하는 것은 pub 클라이언트·Gradle JVM·CocoaPods·Android SDK 도구이며(§2.1 표의 '필요 경로'·'플랫폼' 열), ACP 에이전트는 정의상 별도 프로세스다(seed-spec 22행: JSON-RPC 2.0 over stdio). Dart 프로세스 안에 HTTP 인터셉터를 두어도 그 요청들은 우리 프로세스를 지나가지 않는다.

→ DD-14: 집행 지점은 프로세스 경계다. 인프로세스 인터셉트는 agent 자신의 LLM 호출에만 유효하며, 그것은 AC-14 가 겨냥하는 표면의 일부일 뿐이다.

5.2 집행 지점 4개와 각각의 대상

대상 무엇을 집행 어디서
에이전트의 파일 편집(도구 호출/fs/write_text_file) 경로 술어 + EditSnapshot workspace 게이트(인프로세스)
에이전트가 실행하는 명령(빌드·테스트·pub get·ACP terminal) 경로 + 호스트 toolchain 런처(프로세스 경계)
ACP 에이전트 프로세스 자체 경로 + 호스트 같은 런처
사람이 여는 터미널 집행하지 않는다 AC-14 의 Given 은 "에이전트가 작업을 수행할 때"다(seed-spec 116행). 사람의 터미널을 여기서 제한하면 경계를 넓히는 것이 된다

DD-15 — 단일 런처 규칙 (rev.2 정정): 워크스페이스 코드에서 Process.start/Process.run 을 직접 호출할 수 있는 곳은 toolchain 의 ProcessLauncher 구현 파일 하나뿐이다. 그 밖의 모든 곳은 core.ProcessLauncher 계약만 호출한다. 같은 방식으로 dart:io 파일 쓰기 API 직접 호출은 workspace 의 저장소·게이트 구현 파일과 bricks 의 GeneratorTarget 구현 파일로 한정한다(§4.6). custom_lint 규칙 1개가 허용 파일 목록을 SoT 로 두고 CI 가 강제한다(조직에 bloc_signals_lint 선례가 있으므로 방식이 새롭지 않다). ⚠️ 그 선례는 이 리포에 없다 — §2.1 DD-05 의 구현 주석(#17) 참조. 집행 수단은 .github/scripts/check_*.py 가드로 읽는다.

📌 구현 주석 (#22, 2026-09-11) — dart:io 쓰기 허용 파일 목록의 SoT 는 check_dart_io_write_allowlist.py 의 ALLOWLIST 다(회귀 고정 test_check_dart_io_write_allowlist.py). 판정 대상은 package/cocode_*/{lib,bin}/** 이며 app/cocode 는 그 앱이 core 를 의존하기 시작하면 자동 편입된다. CI 잡으로의 승격(위반 0건 보고)은 #31 이 맡는다. 게이트 구현은 workspace 의 FileWorkspaceGate, 런타임 쪽 요청 통로는 core 의 FileEditRequested.operation + ApplyFileEditUseCase(DD-02 C2).

⚠️ 초판은 "어디에서도 직접 호출하지 않는다"로 적어 런처와 게이트의 구현 자체를 금지했고, package:mason 코어 인프로세스 채택과도 충돌했다. 허용 파일 목록을 명시해 해소했다.

📌 구현 주석 (#34, 2026-09-12) — DD-15 의 앞 절반(단일 런처) — Process.start/Process.run 허용 파일 목록의 SoT 는 check_process_launch_allowlist.py 의 ALLOWLIST 다(회귀 고정 test_check_process_launch_allowlist.py, CI changes 잡). 구현은 toolchain(L1 신설)의 ToolchainProcessLauncher 이고 그 파일 하나만 목록에 있다. 판정 대상은 #22 가드와 같은 package/cocode_*/{lib,bin}/** 이며 app/cocode 는 그 앱이 core 를 의존하기 시작하면 자동 편입된다.

착수 시점의 직접 호출 5곳 처분 (#34 인수조건 2 — 판정 없이 lint 를 켜면 CI 가 즉시 붉어져 규칙 자체가 무력화된다). 전부 package/cocode_* 밖이라 검사 대상 제외이며, 그 근거는 DD-15 가 말하는 「워크스페이스 코드」가 cocode ADE 가 자식을 띄우는 런타임 경로라는 것이다:

호출 지점 처분 근거
backend/cocode_server/lib/src/feature/file/util/storage_util.dart:1369 검사 대상 제외 도너 유래 Serverpod 백엔드가 부르는 aws CLI. 에이전트의 자식이 아니고 D-031 이 도너 표면을 분석·빌드 범위에서 제외했다
backend/cocode_server/test/unit/tool/seed_e2e_accounts_guard_test.dart:32 검사 대상 제외 같은 도너 표면의 테스트
scripts/generate_openapi_packages.dart:29·:52 검사 대상 제외 리포 코드제너레이션 스크립트(mason 호출) — 제품 런타임이 아니다
scripts/update_resolvable_versions.dart:31 검사 대상 제외 같은 유형의 리포 유지보수 스크립트

그 뒤 #25·#31 이 더한 package/workspace/test/** 의 Process.runSync('chmod', …) 는 허용 파일 목록 등재다 — 픽스처 권한을 만들어야 게이트·롤백 테스트가 성립하며, #22 가드가 package/cocode_*/test/ 를 등재한 것과 같은 이유다. .github/scripts/probe_windows_atomic_replace.dart(#27)도 package/cocode_* 밖이라 대상이 아니다. 이 처분은 그 스크립트의 회귀 테스트가 경로별로 고정한다 — 표만 두면 판정 대상 규칙이 바뀔 때 조용히 어긋난다.

환경변수 화이트리스트(DD-23a)는 프로파일과 무관하게 적용된다. §5.2 표의 「집행하지 않는다」가 걸린 열은 「무엇을 집행」 = 경로 + 호스트이고 AC-14 의 Given 에 종속되는 반면, DD-23a 는 §7.1(보안) 항목이라 Given 이 다르다 — 사람이 여는 터미널에서도 ANTHROPIC_API_KEY 는 자식으로 전달되지 않는다. SandboxProfile·DesktopPlatform 은 core(L0)에 있고(런처와 SandboxProfileRegistry 가 둘 다 L1 이라 서로를 의존할 수 없다), SandboxProfileRegistry 는 workspace 가 소유하며 그 키 전수와 「humanTerminal 은 집행을 끈다」는 사실을 core 의 완결성 테스트 축 4 가 검사한다(DD-02a).

런처는 샌드박스 프로파일을 인자로 받는다(SandboxProfile.agent / SandboxProfile.humanTerminal). 사람의 터미널은 후자로 기동해 집행을 끄고, 그 사실이 SandboxProfileRegistry 완결성 테스트의 검사 대상이 된다.

5.3 네트워크 (AC-14 ⓑ)

DD-16 — 로컬 정책 프록시. 런처는 자식 프로세스에 HTTP_PROXY/HTTPS_PROXY/NO_PROXY 를 주입하고, 프록시가 CONNECT/요청 단위로 목적지 호스트를 allowed_hosts 와 대조한다.

  • DD-17(판정 없는 규칙): 리다이렉트 여부와 무관하게 모든 목적지 호스트를 같은 규칙으로 판정한다. "체인의 중간이라서" 예외를 주지 않는다. 이것이 PRD 406행이 "근거 문서가 실제로 지지하는 판정 없는 규칙"으로 남긴 것과 결이 같다 — 즉 실측으로 확인된 중간·최종 호스트(release-assets.githubusercontent.com, plugins-artifacts.gradle.org, repo.maven.apache.org, dl.google.com — planning-inputs §2.2, 그리고 §2.1 42행이 방어적 허용을 권한 objects.githubusercontent.com)를 기본 목록에 미리 넣는다.

  • DD-17a — 미지 리다이렉트 대상의 처리 (rev.2 정정): 차단 + 작업 실패(fail-closed), 그리고 사후 제시. 초판은 "PRD D-22 는 이로써 '차단 후 사용자 승인' 경로로 확정된다"라고 적었다. 철회한다. 셋 다 성립하지 않았다:

    1. DD-17 의 규칙에서 따라 나오지 않는다. DD-17 은 무엇을 판정하는가를 정할 뿐 놓쳤을 때 무엇을 하는가를 정하지 않는다. PRD D-22(649행)가 남긴 세 선택지(차단 후 사용자 승인 / 도메인 스코프 확장 / 실패) 중 하나를 고르는 것은 별개 결정이다.
    2. 표현할 상태가 없다. Seed Spec §3 의 pending_approval_kind 는 없음 | 완료승인 | 테스트변경승인 폐쇄 집합이고, §3 머리말은 잠금 후 기존 엔티티 수정을 금지한다. 네트워크 승인 대기를 나타낼 값이 없다.
    3. 비동기 위임과 충돌한다. C-04 는 사람이 자리를 비운 상태를 전제하고 AC-08 은 시도에 시간 상한을 강제한다. 자식 프로세스의 HTTP CONNECT 를 사람이 돌아올 때까지 붙잡아 둘 수 없고, 도구 쪽 타임아웃이 먼저 끊는다.

    따라서 규칙은 이렇다 — 미등록 호스트로의 요청은 즉시 차단하고, 차단으로 인해 그 시도가 실패하면 작업은 AC-07 의 재시도 경로를 거쳐 최종적으로 "실패"로 전이한다. 차단된 호스트 목록은 작업 상세에 제시되며, 사용자는 돌아온 뒤 그것을 allowed_hosts 사용자 추가분(seed-spec 70행)에 넣고 재실행할 수 있다. 승인은 작업을 붙잡지 않고 사후에 이뤄지므로 새 상태값이 필요 없다. 이는 PRD NFR-C(779행 이하)가 "미선언 호스트 fail-closed / 차단 방향으로 실패"라고 기록한 방향과 같다.

  • 프록시는 planning-inputs §2.2 의 함정 3종을 자동으로 통과한다(체인의 각 호스트가 별도 판정 대상이 되므로).

  • 기본 목록: planning-inputs §2.1 표의 행 집합을 '플랫폼' 열로 필터한 결과를 그대로 쓴다(PRD NFR-C 가 정한 "사람의 선별이 개입하지 않는" 형태). 그 표가 단일 SSOT 이고 개수는 쓰지 않는다 — 개수를 정본으로 두지 않기로 확정됐다(D-051, #39 · 오너 결정 #109). 개수를 판정 기준으로 인용하는 것은 문서·코드·테스트 어디서도 금지이며, 판정은 「비어 있지 않다」 + 대조 검사로만 한다. "타겟하는 플랫폼"의 판정 입력은 워크스페이스의 플랫폼 디렉터리 존재로 확정됐다(D-052, #39 — PRD D-21 · §9 미결 16 해소).

📌 구현 주석 (#36, 2026-09-12) — 기본 목록 합성과 경로 해석 — core 의 SandboxDefaults 가 planning-inputs §2.1·§2.4 표를 그대로 들고 hostsFor(targets) 로 플랫폼 필터를, composeAllowedHosts 로 「기본 · 파생 · 사용자 추가분의 합」을 만든다. 의존성 파생분의 추출 알고리즘은 D-050 ①(#39) 이 확정했다(lock 우선 · 전이 포함 · 게이트가 매니페스트 변경을 알리는 시점에 재계산 · 세션 고정). 이 함수는 그 산출물을 derived 로 받기만 한다 — 추출은 이 층의 일이 아니다.

🔒 개수를 인용하지 않는다 — 영구 규칙 (D-051, #39 · 오너 결정 #109, 2026-09-13). 코드·테스트 어디에도 「N종」이 없다. 이것은 미결을 피하는 임시 우회가 아니라 확정된 규칙이다: 개수를 정본화하지 않고 목록 자체를 단일 SSOT 로 둔다. 근거는 실측이다 — #38 의 3-OS 실측이 §2.1 표에 없는 호스트를 찾았고(CocoaPods 가 api.github.com 을 실제로 부른다), 같은 실측이 Gradle/JVM 은 프록시를 무시하고 직접 나간다는 것도 확인했다. 어떤 수를 고정해도 다음 실측에서 같은 불일치가 재발하므로 판정은 「비어 있지 않다 + 대조 검사」로만 한다(#36 인수조건 6). api.github.com 과 trunk.cocoapods.org 는 기본 목록에 넣었고, D-051 에 따라 §2.1 표도 같은 두 행을 갖는다(출처 「#38 3-OS 실측」) — 표↔코드 행 집합 일치는 package/core/test/domain/host_list_ssot_test.dart 가 고정한다.

✅ 「타겟하는 플랫폼」 판정 입력 확정 (D-052, #39). hostsFor(targets) 의 targets 는 워크스페이스의 플랫폼 디렉터리 존재로 정해진다 — 루트 바로 아래 · 이름 정확 일치 · 하나도 없으면 빈 집합(전 플랫폼 합집합으로 되돌아가지 않는다). 규칙은 core 의 BuildTargetDetection(순수), 디스크를 보는 쪽은 workspace 의 WorkspaceBuildTargets 다(Directory.existsSync 만 — DD-15 쓰기 목록 대상 아님). SandboxDefaults 자신은 여전히 타겟 집합을 인자로 받는다: 판정에 IO 가 필요하고 이 층은 순수하기 때문이다.

⚠️ www.google-analytics.com 은 넣지 않았다. #38 실측에서 pub 이 실제로 치지만 planning-inputs §2.3 이 「차단해도 되는 호스트(텔레메트리 — flutter config --no-analytics 권장)」로 분류한 행이다. 기능 경로가 아니라 텔레메트리이므로 차단이 기본이고, 막혀서 pub 이 실패한다면 그때 §2.3 의 분류가 틀린 것이므로 그 표를 고쳐야 한다 — 목록을 조용히 넓히는 쪽으로 풀 문제가 아니다.

write_exceptions 해석은 toolchain 의 WriteExceptionResolver 다(DD-19). 경로를 하드코딩하지 않고 ① 재지정 변수(PUB_CACHE 등) ② 재지정 변수가 없는 2종은 실행 시점 해석 ③ 플랫폼 기본값 조립 순으로 푼다. 해석에 실패한 경로는 예외 목록에 넣지 않는다 (차단 방향) — 넓은 기본값으로 대신 채우면 예외가 의도보다 넓어져 AC-14 ⓐ 가 느슨해진다. 실행 시점 해석기는 주입받는다: flutter --version 을 띄우는 일이라 런처를 지나야 하고 (DD-15), 순수 해석기가 그것을 직접 하면 층이 섞인다.

설정 화면 S-16 은 모델까지만 이 Story 가 놓는다 — SandboxSettings(편집 연산 · 폐쇄 거부 사유). 위젯이 사는 ui 와 셸은 Epic #53 산출물이고 아직 없다(완결성 테스트 축 5 의 활성화 조건이 그 부재다). #34 가 SandboxProfileRegistry 를 데이터로 먼저 놓은 것과 같은 방식이다. 편집 대상은 사용자 추가분뿐이며 기본 목록 행은 읽기 전용이다 — 지울 수 있게 하면 다음 생성에서 다시 채워지거나 빌드가 조용히 깨진다. 차단 호스트의 화면 처분은 UX-D-24(이력이 아니라 추가 후보).

  • 한계 1(실측으로 확정 — #38): 프록시 환경변수를 무시하는 도구가 실제로 있다. docs/sandbox-limits-cocode.md §3 의 3-OS 실측: pub·CocoaPods·Android SDK 도구는 존중하고, Gradle/JVM 은 무시한다 — 빈 GRADLE_USER_HOME 에서 의존성을 받아 왔는데 프록시에는 한 건도 오지 않았다(JVM 은 HTTP_PROXY 가 아니라 -Dhttp.proxyHost 를 읽는다). 대응: 런처가 JAVA_TOOL_OPTIONS/GRADLE_OPTS 로 -Dhttp(s).proxyHost/Port 를 함께 주입하고 그 두 변수를 DD-23a 화이트리스트에 추가한다 — 지금은 목록에 없어 주입해도 자식에게 닿지 않는다(후속 이슈, 같은 문서 §6). §9 미결 4 는 이로써 해소됐다.
  • 한계 2: 프록시를 무시하고 직접 소켓을 여는 프로세스는 OS 레벨 차단이 없으면 막히지 않는다 → §9 미결 6.

📌 구현 주석 (#35, 2026-09-12) — DD-16 로컬 정책 프록시 — toolchain 의 PolicyProxyServer 가 루프백(127.0.0.1 전용 — 0.0.0.0 에 열면 같은 네트워크의 다른 기기가 이 프록시로 나갈 수 있어 샌드박스가 아니라 열린 중계기가 된다)에 서고, 런처가 집행 프로파일의 자식에게 HTTP_PROXY/HTTPS_PROXY/NO_PROXY(+ 소문자)를 주입한다. 판정 술어는 core 의 HostAllowList(순수)이며 #36 의 기본 목록 합성과 설정 화면이 같은 값을 소비한다.

DD-17 은 특별 처리가 아니라 「하지 않음」으로 구현된다. 프록시는 리다이렉트를 대신 따라가지 않는다 — HTTPS 는 CONNECT 마다, 평문 HTTP 는 요청마다 판정하고 3xx 는 클라이언트에게 그대로 돌려준다. 클라이언트가 다음 홉을 다시 프록시로 보내므로 체인의 모든 호스트가 같은 규칙을 지난다. 프록시가 체인을 대신 완주하면 두 번째 이후 호스트가 판정을 건너뛰고, 그것이 planning-inputs §2.2 의 함정 3종이 실제로 지나는 경로다.

⚠️ 그 성질을 지키려면 연결을 재사용하면 안 된다. 이 프록시는 연결 하나당 목적지를 한 번 판정하므로, keep-alive 를 그대로 두면 클라이언트가 리다이렉트를 같은 연결로 이어 보내고 그 두 번째 요청이 이미 첫 목적지로 연결된 파이프에 흘러들어 판정을 건너뛴다. 실측으로 그 상태에서 리다이렉트 테스트 2건이 깨졌다. 그래서 평문 HTTP 는 상류로 넘기기 전에 헤드를 origin-form 으로 고치면서 Connection: close 를 강제한다 — 다음 홉은 반드시 새 연결로 오고, 그래서 반드시 판정된다.

런처와의 결합은 fail-closed 다. SandboxProfile.agent 기동에 정책 프록시가 없으면 ToolchainProcessLauncher 가 StateError 를 던진다. 호스트 집행이 아닌 축을 검증하는 호출부는 allowUnenforcedAgentLaunch: true 로 명시해야 한다 — 조용히 열리는 경로를 두지 않기 위해 기본값을 닫아 두고 예외를 눈에 보이게 했다. humanTerminal 에는 프록시를 주입하지 않는다 (§5.2 표 4행 — 사람의 터미널을 우리 프록시로 몰면 그 사람의 네트워크까지 제한된다).

DD-17a 의 「사후 제시」 데이터는 BlockedHostLog 다 — 차단 사건을 인메모리로 모으고 hostsFor(taskId:) 가 작업 상세에 보일 호스트 이름 집합을 준다(같은 호스트가 여러 번 막혀도 한 번). 영속하지 않는다: .cocode/history 는 편집 스냅샷의 자리이고 거기 넣으면 보존 정책 P1~P7 이 읽지 않는 네 번째 데이터가 생긴다. 차단은 작업을 붙잡지 않는다 — pending_approval_kind 가 폐쇄 집합이라 「네트워크 승인 대기」 상태를 만들지 않는다는 DD-17a 그대로이며, 테스트가 즉시 응답을 고정한다.

⚠️ 이 구현은 AC-14 ⓑ 를 완전히 만족한다고 주장하지 않는다 — 아래 한계 1·2 가 그대로 남아 있고, 그 좁힘은 #38 의 실측(도구별 프록시 존중 여부 · 플랫폼별 격리 가용성)에 종속된다.

5.4 파일 경로 (AC-14 ⓐ) 와 C-02 의 결합

DD-18 — 허용 집합: (워크스페이스 경로 ∪ write_exceptions) − 거부 목록. 거부 목록은 현재 <workspace>/.cocode/** 하나다(DD-09). PRD FR-701(395행)이 채택한 집합식 독법(PRD D-19)을 그대로 쓴다.

DD-19 — 경로 해석: 하드코딩하지 않고 PUB_CACHE·GRADLE_USER_HOME·CP_HOME_DIR·ANDROID_HOME 을 존중한다(planning-inputs §2.4). 재지정 변수가 없는 두 경로($FLUTTER_ROOT/bin/cache, Xcode DerivedData — 같은 표)는 런처가 실행 시점에 각각 flutter --version 의 SDK 경로와 Xcode 설정에서 해석해 세션 값으로 고정한다(PRD D-20 해소 제안). 해석에 실패하면 그 경로는 예외로 추가되지 않는다(차단 방향).

📌 구현 주석 (#37, 2026-09-12) — 허용 경로 술어 — workspace 의 SandboxWritePathPolicy 가 #22 가 남긴 seam(WritePathPolicy)에 DD-18 의 판정을 꽂는다: (워크스페이스 ∪ write_exceptions) − {<workspace>/.cocode/**}. write_exceptions 목록의 합성 규칙은 #36 소유이며 이 술어는 값을 소비만 한다(해석 실패분은 resolver 가 이미 빼고 넘긴다).

AC-14 ⓐ 는 집합식으로 읽는다(PRD FR-701 / D-19). 판정은 「허용 집합에 드는가」 하나이고 확장자·디렉터리 종류를 보지 않는다 — 그 귀결이 FR-104(비-Dart 파일 편집 비제한)이며, 그것을 막는 규칙이 없는 것이 구현이다. 다른 독법(「안도 일일이 허용」)이면 AC-01·C-02 가 전제부터 깨진다. 거부 목록은 루트 바로 아래의 .cocode 만 본다 — 깊은 곳의 같은 이름은 그냥 소스다.

⚠️ 심볼릭 링크를 풀지 않으면 예외가 한 번도 맞지 않는다. 게이트는 대상 경로를 실재 경로로 풀어 판정하므로(_realPath), 예외 목록을 풀지 않으면 /var/folders/… 와 /private/var/folders/… 를 비교하게 된다(macOS 에서 /var 가 /private/var 로 가는 링크다). 실측으로 게이트 스위트가 그 상태에서 3건 실패했고, ~/Library/Caches 계열이 정확히 같은 모양이라 실사용 경로에서도 같은 일이 난다. 술어가 양쪽을 같은 규칙으로 canonicalize 한다.

⛔ write_exceptions 경로의 편집은 술어가 통과시켜도 게이트가 여전히 거부한다 (FileEditRejection.outsideWorkspace). 히스토리에 담을 워크스페이스 상대 경로가 없기 때문이며, 그 기록 표현을 만들려면 롤백이 경로를 p.join(root, relative) 로 되짚는 방식(#25)과 저장 키 공간(#23)까지 바꿔야 해서 #37 의 범위 밖이다. 지금 상태는 더 적게 허용하는 쪽이라 AC-14 를 좁히며 그것은 위반이 아니다. 진전은 두 거부가 다른 코드로 갈린다는 것이다 — 예외에 든 경로(outsideWorkspace)인지 아닌지(pathNotWritable)를 호출부가 구분할 수 있다. 후속 이슈: 워크스페이스 밖 편집의 기록 표현(저장 키 + 롤백 경로 해석).

DD-20 — 프로세스 파일 격리는 스파이크 결과에 종속된다. 자식 프로세스의 쓰기를 실제로 막으려면 OS 수준 격리가 필요하다. 후보는 macOS(Seatbelt 프로파일), Linux(user namespace + bind mount), Windows(Job Object + 제한 토큰) 계열이나 가용성·제약을 이 문서가 검증하지 않았다. 확인 전까지 다음 2단 구성을 쓴다:

  1. 예방 — 캐시 경로 환경변수 고정으로 정상 도구가 워크스페이스 밖을 건드릴 이유를 없앤다.
  2. 검출 — 워크스페이스·예외 경로 밖 쓰기를 파일 감시자로 사후 검출하고, 검출 시 해당 작업을 즉시 실패로 전이시킨다(fail-closed).

2 는 예방이 아니라 검출이다. AC-14 의 "위반 시도가 차단되는 것"을 완전히 만족하지 못한다 → §10-③에 어긋남으로 기록하고, 플랫폼별 격리 가용성 스파이크를 로드맵 1단계(A-03·A-04 스파이크와 같은 회차, master 채널에서 — D-022)에 넣을 것을 제안한다.

C-02 와의 연결: 같은 격리가 확보되면 부수 효과가 하나 더 생긴다 — ACP 에이전트 프로세스에 워크스페이스 쓰기 권한을 주지 않으면, 그 에이전트는 클라이언트 fs 능력(fs/write_text_file)을 쓸 수밖에 없고, 그 경로는 게이트를 지나 EditSnapshot 을 남긴다. 즉 §3.4 가 남긴 C-02 구멍이 같은 장치로 막힌다. 격리가 없으면 감시자 검출로 근사할 수밖에 없고, 그것은 근사다(§10-②).

5.5 OS 샌드박스 — macOS App Sandbox 는 끈다 (DD-27, #299)

위 5.1~5.4 는 ADE 가 자식(에이전트·툴체인)에 거는 격리를 다룬다. 그 위에 OS 가 ADE 자신에게 거는 격리가 하나 더 있고, 이 절이 없어서 그 사실이 문서에 없었다.

결정 (2026-09-21, 오너 위임으로 확정):

항목 값 근거
macOS 배포 경로 Developer ID 직접 배포 + 공증 (Mac App Store 포기) ADE 는 flutter·dart·cob dev·셸을 자식 프로세스로 띄우고 프로젝트 디렉터리·~/.pub-cache·~/fvm 에 닿아야 한다. App Sandbox 안에서는 어느 것도 되지 않는다 — Serverpod App Studio 가 같은 이유로 샌드박스를 끄고 Developer ID + 공증 + Sparkle 로 배포한다(codesign -d --entitlements - 실측)
com.apple.security.app-sandbox false (Debug/Profile/Release 전부) · ENABLE_APP_SANDBOX = NO files.user-selected.read-write + security-scoped bookmark 로는 자식 프로세스가 상속받는 경로가 되지 않는다(bookmark 는 호출 프로세스의 권한이다). 켜 둘 방법이 없다
로컬 debug 서명 ad-hoc (CODE_SIGN_IDENTITY = "-", CODE_SIGN_STYLE = Manual, DEVELOPMENT_TEAM = "") 팀 ID 는 리포에 넣지 않는다. 종전 Apple Development + 빈 팀은 Signing for "Runner" requires a development team 으로 로컬 debug 빌드조차 불가했다(#303 실측)
Release 서명 리포 기본값은 ad-hoc, 공증 파이프라인이 붙을 때 CI 가 DEVELOPMENT_TEAM·CODE_SIGN_IDENTITY = "Developer ID Application" 을 주입 배포 자격(팀·인증서)은 CI 시크릿의 몫이고, 리포 상태만으로 항상 빌드 가능해야 한다
Windows 제약 없음 — MSIX 는 AppData 가상화만 하고 자식 프로세스·사용자 디렉터리 접근을 막지 않는다 desktop-windows-msix.md
Linux 제약 없음 (AppImage/zip 포터블, 샌드박스 없음) —

5.1~5.4 와의 관계: OS 샌드박스를 끄는 것은 ADE 가 자식에게 거는 격리(DD-14·DD-20)를 약화시키지 않는다 — 그 격리는 ADE 가 자식에게 적용하는 것이고, OS 샌드박스는 ADE 자신을 가두는 것이라 둘은 다른 층이다. 오히려 App Sandbox 가 켜져 있으면 DD-20 의 후보(Seatbelt 프로파일로 자식 격리)를 ADE 가 스스로 적용할 수 없다.

검증 (2026-09-21 실측): serverpod start --no-tui --no-docker 가 flutter_apps 설정대로 flutter run -d macos --flavor development 를 띄워 Launching App … done. (37.9s) → Connecting to App VM service done. → Syncing files to device macOS → App is running. 까지 갔다 — #303 PR #430 "동반 앱 부착" 사슬의 마지막 단이 닫혔다. 그 과정에서 걷어낸 것: keychain-access-groups(프로비저닝 프로파일을 요구해 ad-hoc 과 양립 불가 — 샌드박스가 꺼지면 기본 키체인 접근에 필요 없다) · 도너의 FlutterFire: upload-crashlytics-symbols 빌드 단계(ADE 에 Firebase 의존이 없어 flutterfire 스크립트 부재로 매번 실패).


6. (f) 미결 판정 규칙 2건의 해소

6.1 C-04 ⑤ — "①~④ 적용 불가"를 판정에서 제거한다 (DD-21)

문제: C-04 ⑤는 "①~④가 모두 적용 불가한 편집에 한해" 쓸 수 있고, 그 사유를 verification_log 에 남기도록 요구한다(seed-spec 63행). 그런데 무엇이 "적용 불가"인지 규정이 없다(PRD D-2). 에이전트 자신이 판정하면 ⑤가 ①~④를 우회하는 상시 경로가 된다.

해소 — 판정을 실행 결과로 바꾼다.

①~④는 "적용 가능한지"를 판정하지 않는다. 실행한다. 각 방법은 {통과, 실패, 대상없음} 중 하나를 반환하며, 대상없음은 실행했으나 대상이 없어 no-op 인 경우를 뜻한다.

방법 실행자 대상없음의 기계적 조건 사유 코드(폐쇄 집합)
① 빌드 성공 toolchain 워크스페이스에 구성된 빌드 타깃이 없다 no_target:build_no_configured_target
② 테스트 통과 toolchain 테스트 러너가 수집한 테스트 수가 0 no_target:test_zero_collected
③ 핫 리로드 후 런타임 오류 없음 toolchain(vm_service) 연결된 디버그 세션이 없다 no_target:no_debug_session
④ 정적 분석 통과 toolchain(dart analyze 프로세스) 편집 대상 파일이 분석 대상 집합에 포함되지 않는다 no_target:file_not_analyzed

DD-21a — 결과 우선순위 (rev.2 신설, 초판 누락). 초판은 "하나라도 통과면 AC-09 충족", "하나라도 실패면 재시도"를 둘 다 적고 동시 발화 시의 중재를 두지 않았다. 문자 그대로 실행하면 ④가 새 오류를 뱉어도 ①이 성공했다는 이유로 완료 전이가 가능해진다. 순서를 못박는다:

  1. 실패 가 하나라도 있으면 → 재시도 경로(AC-07). 통과 가 함께 있어도 완료로 가지 않는다.
  2. 실패 가 없고 통과 가 하나 이상이면 → 완료 가능(AC-09 "1종 이상 통과").
  3. 넷 모두 대상없음 이면 → ⑤가 열린다.
  4. 실패 도 통과 도 대상없음 도 아닌 상태(미실행)가 남아 있으면 완료로 가지 않는다.

판정 대상 집합(#32, DD-23c-③). verification_log 는 회차별로 누적되므로, 규칙 1~4 가 세는 실패·통과·대상없음·미실행은 현재 회차(attempt_no == retry_count)의 레코드에서만 센다. 앞 회차의 실패 는 그 회차의 재시도 사유였을 뿐 현재 회차를 막지 않고, 앞 회차의 통과 는 현재 회차의 완료 자격이 되지 않는다.

DD-21b — ⑤ 자체의 실행 정의 (rev.2 신설, 초판 누락). 초판은 ⑤를 "열리는 문"으로만 취급하고 ⑤가 무엇을 하는지 적지 않았다. C-04 ⑤(seed-spec 63행)는 3항 술어다 — 파일이 읽히고, 해당 형식의 파서가 오류를 내지 않으며, 정적 분석이 그 파일에 대해 새로운 오류를 보고하지 않음. 실행자는 toolchain 이며:

  • 파서 정본은 FileFormatParserRegistry 가 확장자 → FileFormatParser 계약 구현으로 폐쇄 매핑한다(§2.2). MVP 등록 대상은 §9 미결 5 에서 확정한다(PRD D-12 의 "⑤의 파일 형식별 정본 파서" — 초판이 누락한 항목).
  • 등록된 파서가 없는 형식이면 ⑤도 대상없음 을 반환한다. 📌 그 대상없음을 기록할 코드는 no_target:no_registered_parser 다(#64 → D-053) — 거동은 이 항목 그대로 차단이고 코드 신설은 기록만 연다. MVP 등록 목록(.dart · .yaml/.yml · .json/.arb)은 #65 → D-054 가 정했다. 그러면 다섯 방법 모두 통과가 없으므로 자가검증 완료가 불가능하고, 작업은 재시도 후 실패로 가거나 사람승인 으로 남는다 — 차단 방향이며 C-04 전환 조항의 안전 기본값과 같은 방향이다.
  • 세 번째 절("정적 분석이 그 파일에 새 오류를 보고하지 않음")은 ⑤가 열린 상황(④가 file_not_analyzed)에서는 공허하게 참이다. 이는 Seed Spec 문면이 그렇게 되어 있는 결과이며 이 문서가 좁힌 것이 아니다 → §10-⑫에 기록한다.

③ 의 대상없음 조건을 판정 없는 형태로 바꾼 이유 (rev.2 정정). 초판은 "③의 관찰 창이 미정인 동안 ③은 항상 미실행으로 취급하며, 미실행은 ⑤의 문을 열지 않는다(안전 방향)"고 적었다. 그러면 §9 미결 5 가 풀리기 전까지 ⑤가 도달 불가능한 사문이 된다. Seed Spec 출처표 134행은 "⑤가 항상 대체 경로로 존재하므로 1은 도달 가능한 최소값"이라며 AC-09 의 하한을 정당화하므로, ⑤의 사멸은 C-04 의 폐쇄 5종 목록을 4종으로 줄이는 것과 같다. 고쳤다:

  • ③의 대상없음 은 "연결된 디버그 세션이 없다" 로 정의한다. 이는 사건 존재 여부 비교이며 관찰 창 값이 필요 없다. 비동기 위임 작업에는 통상 디버그 세션이 없으므로 ③은 대개 대상없음 이고, ⑤로 가는 경로가 살아 있다.
  • ③이 미실행이 되는 경우는 "세션이 연결돼 있는데 관찰 창이 미정"일 때뿐이며, 그때는 DD-21a-4 에 따라 완료로 가지 않는다(안전 방향). 그 경우 ④는 거의 항상 대상이 있으므로 실무적 영향이 작다.

이 규칙이 왜 우회를 막는가 — 그리고 무엇을 막지 못하는가:

  1. 에이전트가 판정에 참여하지 않는다. 실행도 코드 기입도 toolchain 이 한다. 에이전트의 "적용 불가합니다"라는 발화는 verification_log 에 들어갈 자리가 없다.
  2. 적용 불가 사유는 자유 서술을 받지 않는다. 위 4개 no_target: 코드 외의 값은 스키마가 거부한다(PRD FR-303 이 요구한 "폐쇄 사유 코드 집합"의 구체화). ⚠️ 이는 verification_log 전체가 아니라 ⑤ 사용 시의 적용 불가 사유 필드에만 해당한다 — §7.1 참조.
  3. ④는 Dart/Flutter 워크스페이스에서 거의 항상 대상이 있다 — C-03 이 1급 프로젝트 타입을 Dart/Flutter 로 한정하므로 dart analyze 는 항상 실행 가능하다.
  4. ④·⑤의 "새로운 오류" 기준선(PRD D-12): 기준선은 그 작업의 첫 편집 직전 상태에서 실행한 dart analyze 결과로 고정한다. 작업 시작 시 1회 수집하며, 에이전트가 그 사이 무엇을 했는지와 무관하다.
  5. 실행 순서는 값싼 것부터 ④ → ② → ① → ③ 이되, DD-21a-1 때문에 조기 종료는 실패 를 확인한 뒤에만 안전하다 — 즉 통과 가 나와도 아직 실행하지 않은 방법이 남아 있으면 완료로 가기 전에 그것들을 마저 실행한다. 초판의 "통과가 나오는 순간 나머지를 실행하지 않아도 된다"는 4번 규칙과 충돌하므로 철회한다.
  6. 막지 못하는 것(정직하게): 통과한 방법이 편집 대상을 실제로 검사했는지는 이 규칙이 보장하지 않는다. CI yaml 한 줄만 고친 작업도 Flutter 앱 워크스페이스에서는 ①이나 ②가 통과해 완료될 수 있다. 이는 Seed Spec C-04/AC-09 의 문면("적용 가능한 것을 1종 이상 통과")이 그렇게 되어 있는 결과이며 Design 이 경계를 넓히지 않고 고칠 수 없다. 이것이 정확히 A-02 가 측정해야 할 실패 모드(에이전트 자신이 만든 결함에 대한 검출률 ⓑ)이므로 A-02 스파이크 설계에 이 시나리오를 넣을 것을 제안한다 → §10-⑬.

📌 구현 주석 (#66, 2026-09-13) — 재시도 3회 상한과 자가 검증 시간 상한의 집행

상한 3종은 core 의 VerificationLimits 한 곳에 모인다 — 시도 시간 상한 (기본 20분, D-033 · planning-inputs §7.1) · retry_count 상한(3, C-04 본문) · 환경 재실행 예산(기본 0, D-055 §5.2 의 「미정 시 동작」). 값을 코드에서 지어내지 않았고 셋 다 설정 항목으로 노출된다. 생성자가 attemptTimeout <= 0 을 거부하므로 AC-08 의 「미설정·무한대 불허」가 타입 수준에서 성립한다.

시간 초과는 전이가 아니다 (D-056). SelfVerificationTimedOut 사건을 제거했다 — 시간 초과는 VerificationLimits.timedOutRecord 로 result: 실패 · failure_class: code_defect(D-056 §6 갈래 0) 레코드를 남기고, 그 뒤 기존 SelfVerificationFailed 가 ⑧(재시도) 또는 ⑪(예산 초과)로 보낸다. 첫 초과가 작업을 죽이지 않는다 — D-033 이 계산한 「20분 × 3 = 60분 상계」가 그렇게 성립한다.

환경 실패는 retry_count 를 쓰지 않는다 (D-055 §4). TaskTransition 이 VerificationVerdictEvaluator 로 그 회차의 분류(우선순위 최댓값 하나)를 구해, 계수하지 않는 분류면 retry_count 를 그대로 둔 채 ⑪ 로 보낸다. ⚠️ 판정할 레코드가 없으면 계수한다 — 신호 없이 안전장치를 느슨하게 하지 않는 보수적 기본값이다(D-055 §3.2 의 포괄 갈래와 같은 논거).

새 전이 칸이 생기지 않았다. VerificationEnvironmentExhausted 는 출발·도착이 자가검증중 → 실패 로 ⑪ 과 같고 retry_count 를 건드리지 않는 점만 다르다.

⚠️ EditingTimedOut 의 거부 사유가 바뀌었다 — 「값이 없어서」(owner #106)에서 「규칙이 없어서」(owner 없음 = 영구 거부)로. D-059 §1.1 이 편집 단계에 자동 상한을 두지 않기로 확정했고, 그 자리는 DD-22 S2 와 AC-22 중지 입력이 덮는다. 상한을 AC 로 신설할지는 프로젝트 오너 판정(B-07)이다.

📌 구현 주석 (#68, 2026-09-13) — 사람승인 완료 흐름과 C-04 전환 조항 기본값

A-02 상태를 읽어 completion_mode 를 정하는 자리는 CompletionModePolicy 하나다 — DD-05 가 status·pending_approval_kind 에 세운 원칙을 그대로 적용했다. 자리가 둘이면 한쪽이 전환 조항을 빠뜨렸을 때 A-02 미검증 상태에서 자가검증 완료가 열린다. 리포 스캔 테스트가 A02VerificationState 참조를 정책 파일 밖에서 찾으면 실패한다(#68 인수조건 5).

전환 조항의 세 경우를 하나로 접지 않았다 — criteriaUnset·notPerformed· belowThreshold 는 서로 다른 사건이고 화면에 다른 사유로 제시된다. 네 번째 값 met 만이 자가검증 기본값을 연다.

completion_mode override 는 항상 false 다 — 작업 가설(D-057 §4 · PRD D-18, 확정은 #111). #111 이 ③(전환 조항 미발동 시에만 허용)으로 결론 나면 allowsUserOverride 한 함수만 고치면 된다 — 판정 지점이 하나인 이득이 그것이다.

status 폐쇄 집합이 코드에서도 9종이 됐다. D-032(seed v3.3.0)가 2026-09-10 에 개정했는데 코드는 7종이었다 — 그 드리프트를 닫았다. 전이 7종(⑰⑱⑲⑳㉑㉒㉓)이 함께 들어오고 전수 테스트가 81칸 전체를 대조한다(⬜ 칸 0개).

승인 화면은 「관문이 아님」을 타입으로 드러낸다 — CompletionApprovalPresentation .canApprove 가 verificationLog 를 읽지 않는다. 관문이 되려면 읽어야 하는데 읽지 않는 것이 불변식 2·FR-307 의 구현이다. 게이트 조건은 UX-D-13 의 「승인 대상 변경이 비어 있지 않다」 하나다.

⚠️ 테스트변경승인 «승인» 후 복귀는 실행중 이다(D-057 §3.2.1) — 자가검증중 이 아니다. 테스트 파일이 바뀌면 C-04 ②의 대상 자체가 바뀌므로 이전 검증 결과가 무효이고, 되돌아가 이어서 검증하면 옛 테스트 기준의 판정을 쓴다. retry_count 는 증가하지 않는다 — 사람의 승인이 재시도 예산을 먹으면 안 된다.

📌 구현 주석 (#69, 2026-09-13) — 경로 기반 테스트 파일 보호 게이트

판정은 core 의 TestFileProtection 하나가 들고, 게이트는 「지금 그 경로에 정규 파일이 있는가」만 공급한다. 그 한 입력이 생성과 수정을 가르며 내용은 읽지 않는다(해시도 보지 않는다 — FR-403). 단위는 DD-25 R6 의 투영 전이라 삭제·이름변경도 자연히 대상이 된다 — 삭제를 빼면 「삭제 후 재생성」으로 C-05 가 통째로 우회된다.

보류는 «거부» 가 아니다. FileEditHeldForApproval 을 새로 뒀다 — 거부는 「이 편집은 성립하지 않는다」이고 보류는 「사람이 승인하면 그대로 적용된다」 다. 보류가 돌아왔다는 것이 곧 파일 내용이 바뀌지 않았다는 관찰 가능한 형태이며, 스냅샷도 남지 않는다(적용되지 않은 변경은 EditSnapshot 이 아니다 — 그 자리는 PendingChange 이고 저장 설계는 §9 미결 23).

completion_mode 를 받을 «자리» 를 두지 않았다 — 불변식 3 이 「completion_mode 와 무관하게」를 규정하므로, 인자가 없는 것이 그 규정의 구현이다(#69 인수조건 5).

⚠️ 경로 술어는 «어느 세그먼트든» 으로 읽었다. 루트 바로 아래로 좁히면 이 저장소 같은 모노레포에서 테스트 대부분이 보호 밖으로 나가고(테스트는 각 패키지의 test/ 에 산다) 에이전트가 패키지 안에서 작업하는 것만으로 C-05 가 우회된다. C-05 본문이 「모든 변경」을 요구하므로 넓은 쪽이 문면에 맞는 읽기다 — Seed Spec 문면이 세그먼트 깊이를 명시하지 않아 이 판정을 기록해 둔다.

⚠️ 집행은 §5.4 프로세스 격리에 종속된다(#69 인수조건 7) — 이 규칙은 게이트를 지나는 편집에만 걸리고, 우회를 막는 것은 단일 런처(DD-15)·쓰기 허용 파일 목록· 샌드박스 경로 집행(#37)이다. 게이트 밖 삭제·이름변경(터미널 rm·mv)의 처분은 §9 미결 19 이며 이 구현이 그 구멍을 넓히지도 메우지도 않는다.

6.2 AC-20 — "응답 불가" 감지 기준 (DD-22)

문제: AC-20 은 "그 상태가 감지되면"이라고만 쓰고 감지 기준을 규정하지 않는다(PRD D-3, BDD U-9). U-9 는 "무한 대기하지 않는다"의 시간 경계도 같은 항목에 넣는다.

해소 — 판정이 필요한 신호를 쓰지 않는다. 감지기는 §3.1 의 이벤트 스트림 하나만 본다.

신호 정의 판정 개입 백엔드
S1 전송 종료 TransportClosed — 자식 프로세스 exit, stdio EOF, 스트림 종료, 인증 거부 없음(사건 발생 여부) 주로 acp
S2 유휴 초과 마지막 AgentEvent 이후 경과 시간 > 유휴 상한 없음(시각 비교) 양쪽
  • 연속 오류 횟수는 감지 기준으로 쓰지 않는다. "이 오류가 재시도 가능한가"를 판정해야 하고, 그 판정이 곧 분쟁 지점이 되기 때문이다. 프로바이더 SDK 의 자체 재시도는 S2 의 시계 안에서 일어난다.
  • 호스트가 미완료로 붙잡은 AgentRequest 는 S2 의 대상이 아니다 — 그것은 우리 쪽 지연이지 에이전트의 응답 불가가 아니다. §3.1 의 요청 수준 타임아웃이 별도로 처리하며, 그 타임아웃은 AgentRequest.fail() 로 에이전트에게 되돌아간다.
  • 전이 대상: PRD §1.6(374행)의 연역대로 실패다 — 불변식 1·2가 완료를, 불변식 5가 롤백됨을 배제한다. 이 문서는 그 연역의 반례를 찾지 못했다("응답 불가 상태에서 불변식 1 또는 2의 조건이 이미 충족된 경로"는 존재하지 않는다 — 두 조건 모두 응답 불가 사건이 아니라 검증 통과 또는 사용자 승인을 요구한다). PRD 374행이 요구한 확인 기록이 이 문장이다.
  • 유휴 상한 값 (rev.2 정정): 근거가 없다. 새 숫자를 만들지 않기 위해, 기본 시드로 AC-08 이 존재를 강제하는 "자가 검증 시도 시간 상한"의 출하 기본값을 재사용한다(seed-spec 110행). 두 값은 별개 설정 항목이며, 실측 후 각각 조정된다(D-020 원칙에 따라 Planning). ⚠️ AC-08 의 기본값 자체가 아직 선택되지 않았다(PRD D-4, D-020, planning-inputs §1 24행). 따라서 §9 미결 2 의 "미정 시 동작"은 "AC-08 을 시드로 쓴다"가 아니라 "AC-08 이 요구하는 양의 유한한 출하 기본값을 누가 언제 최초로 고르는가" 이며, 그것을 §9 미결 2 에 그대로 적는다. 시드 재사용은 근거의 이전이 아니라 "지어내지 않기 위한 선택"임을 명시한다.
  • 채택 여부 확정(#50): 이 안은 D-060 으로 채택됐다 — 전문은 unresponsive-detection-cocode.md §1. ⚠️ 비준 주체인 오너 결정 #112 는 미결이라 그 채택은 작업 가설이며, 상한 3종의 관계는 같은 문서 §3(D-061)에 있다.
  • 이 규칙으로 BDD Feature 3 의 AC-20 Scenario Outline(백엔드 2종 — bdd 340~343행)이 실행 가능해진다 — S1·S2 중 하나라도 발생하면 실패 전이 + 사유 제시이며, S2 상한이 유한하므로 "무한 대기 상태로 남지 않는다"가 성립한다.

7. (g) 보안

7.1 LLM 프로바이더 자격증명 (DD-23)

규칙 내용
저장 위치 OS 자격증명 저장소(macOS Keychain / Windows 자격증명 관리 / Linux Secret Service). 구체 패키지 선정은 확인 필요(§9 미결 7)
금지 워크스페이스 안(.cocode/ 포함) 저장 금지. 평문 파일 금지. 프로젝트 설정 파일 금지
상주 범위 agent 는 인프로세스이므로 키는 cocode ADE 프로세스 메모리에만 존재한다. 자식 프로세스에 환경변수로 전달하지 않는다
환경변수 상속 런처는 기본 거부 후 화이트리스트만 통과시킨다. 판정이 필요 없다 — 목록 대조뿐
로그 유출 프로세스 출력은 UI 표시용으로만 쓰고 스냅샷·히스토리에 그대로 적재하지 않는다. verification_log 는 아래 규정을 따른다

구현 (#46). 이 표의 규칙은 toolchain 의 OsCredentialVault 가 집행한다 — CredentialStoreBackend 포트 아래 세 백엔드(macOS /usr/bin/security · Linux secret-tool · Windows advapi32.dll FFI)가 붙고, 저장소를 쓸 수 없으면 CredentialVaultUnavailable 을 던지고 멈춘다. 평문 파일·워크스페이스·환경변수로 물러서는 경로는 계약에 없다 — 폴백을 두면 이 표의 「금지」가 「저장소가 있을 때만」 지켜지는 규칙이 되고, 정확히 저장소가 없는 환경에서 평문으로 떨어진다. 선정 근거·백엔드별 실측은 D-064.

⚠️ 이 절의 요건은 Seed Spec §4 의 AC 가 아니다. AC-06 은 두 백엔드 경로의 제공만 규정하고 자격증명의 저장 위치·금지 규칙을 다루지 않는다. §7.1 의 표와 DD-23a·DD-24 는 이 설계가 추가한 결정이며(#46 인수조건 5), 그래서 값이 아니라 규칙으로 여기에 산다. Seed Spec 을 고치지 않고도 성립하고, 뒤집으려면 이 문서의 결정을 뒤집으면 된다.

DD-23a — 환경변수 화이트리스트 (rev.2 정정). 초판은 통과 목록을 PATH·언어/로케일·프록시 변수·캐시 경로 변수로 적었다. 그대로 구현하면 모든 자식 프로세스가 기동하지 못한다 — planning-inputs §2.4 의 캐시 경로는 전부 ~/.pub-cache·~/.gradle·~/.cocoapods·~/.android 처럼 HOME 상대 기본값이고 PUB_CACHE 등은 재지정 변수일 뿐이다. 최소 화이트리스트를 명시한다:

  • 공통: PATH, HOME, LANG/LC_*, TZ, TMPDIR/TMP/TEMP, SHELL(POSIX), TERM(터미널 기동 시)
  • Windows 추가: USERPROFILE, LOCALAPPDATA, APPDATA, SystemRoot, SystemDrive, ComSpec, PATHEXT, NUMBER_OF_PROCESSORS
  • macOS 추가: DEVELOPER_DIR(Xcode 선택), SDKROOT(해석된 경우)
  • 툴체인 재지정(§5.4 로 해석된 값): PUB_CACHE, GRADLE_USER_HOME, CP_HOME_DIR, ANDROID_HOME, FLUTTER_ROOT
  • 프록시(§5.3 로 주입): HTTP_PROXY, HTTPS_PROXY, NO_PROXY (+ 소문자 변형)
  • 명시 거부: 위 목록 밖 전부. 특히 ANTHROPIC_API_KEY·OPENAI_API_KEY 등 자격증명성 변수는 이름 패턴과 무관하게 목록에 없으므로 자동 거부된다.

DD-23b — verification_log 의 스키마 (rev.2 정정). 초판은 "verification_log 는 폐쇄 코드 집합만 담으므로 자유 텍스트 유출 표면이 구조적으로 없다"고 적었다. Seed Spec 과 모순된다. §3(78행)은 verification_log(**수행한 자가 검증 방법과 결과**, ⑤ 사용 시 ①~④ 각각의 적용 불가 사유) 로 규정하고 AC-09(111행)는 "그 결과가 verification_log 에 기록된 뒤에만 완료"를 요구한다. 4개 no_target: 코드만 담으면 통과/실패 결과를 담을 수 없어 AC-09 가 판정 불가가 된다. PRD FR-303(326행)이 폐쇄 코드를 요구한 대상은 ⑤ 사용 시의 적용 불가 사유뿐이다. 스키마를 이렇게 둔다:

{"method":"④","result":"통과|실패|대상없음",
                     "attempt_no":1,                                    // #32 DD-23c-③ — 이 레코드가 속한 시도. EditSnapshot.attempt_no 와 같은 시계(최초 0)
                     "no_target_code":"no_target:file_not_analyzed",   // result=대상없음 일 때만, 폐쇄 **5종** (#64 · D-053)
                     "failure_class":"failure:code_defect",            // result=실패    일 때만, 폐쇄 **5종** (#73 · D-055)
                     "started_at":"…","duration_ms":1234,
                     "diagnostics_count":{"error":0,"warning":2},       // 개수만. 원문 메시지는 담지 않는다
                     "exit_code":0}
                    
  • method·result·no_target_code·failure_class 는 전부 폐쇄 enum이다.
  • no_target_code 는 5종이다 (#64 → D-053). ①~④ 의 4종에 ⑤ 자신의 대상없음 코드 no_target:no_registered_parser 가 더해졌다 — DD-21b 는 ⑤ 대상없음의 거동(차단)만 정하고 기록 값을 정하지 않아 스키마에 구멍이 있었다. 값 집합의 SSOT 는 verification-no-target-cocode.md §5.
  • failure_class 가 신설됐다 (#73 → D-055). result 를 4값으로 늘리는 대신 result=실패 를 정련하는 필드를 둔다 — result 확장은 DD-21a 의 네 규칙 전부가 「새 값은 어느 쪽으로 세는가」를 다시 답하게 만든다. no_target_code 와 대칭이라 스키마에 패턴이 하나만 남는다. 값 집합과 판정 절차의 SSOT 는 verification-failure-class-cocode.md §3.
  • 진단·테스트 결과는 개수와 exit code 만 담고 원문 메시지·스택·경로 문자열은 담지 않는다. 유출 표면 축소는 여기서 나오며, "구조적으로 없다"가 아니라 "자유 텍스트 필드를 두지 않는다"가 정확한 주장이다.
  • 사람이 볼 원문은 UI 세션 버퍼에만 남기고 .cocode/history/ 에 적재하지 않는다. 그 "세션 버퍼"의 정의(보유 주체·수명·상한 동작)와 "종결된 작업의 원문은 사후에 조회되지 않는다"는 귀결은 아래 DD-23c(#32)가 정한다.
  • 레코드는 회차별로 누적된다(attempt_no, DD-23c-③) — 한 작업의 verification_log 는 (회차, 방법)당 1건의 append-only 리스트이며 재시도가 앞 회차를 덮어쓰지 않는다. 스키마 정본의 확정과 코드 반영은 #63 소유이고 #32 는 이 필드의 결정만 산출한다(소유 경계 — no_target_code 값 집합은 #64, 환경 실패 분류는 #73).

📌 구현 주석 (#63, 2026-09-13) — 자가 검증 실행기와 verification_log 스키마

스키마는 core 의 VerificationRecord 다. 생성자가 정합성을 강제한다 — no_target_code 는 result=대상없음 일 때만 있고 그 코드의 방법이 method 와 같아야 하며, failure_class 는 result=실패 일 때만 있다. 그래서 「코드 없는 대상없음」도 「분류 없는 실패」도 표현 불가능하고, 검사를 잊은 호출부가 애초에 만들어지지 않는다. toJson() 의 문자열 값은 전부 폐쇄 enum 의 wire 값이라 자유 텍스트 필드가 0개임을 테스트가 값 집합 대조로 고정한다.

판정은 VerificationVerdictEvaluator 하나가 한다. DD-21a 규칙 1~4 를 그 순수 함수에 모았고 DelegatedTask.canCompleteBySelfVerification 은 그것을 호출한다(재구현 금지 — DD-05 와 같은 원칙). 판정 대상은 현재 회차(attempt_no == retry_count)뿐이다. ⚠️ 이 이관으로 불변식 1 의 판정이 좁아졌다 — 이전 구현은 「통과 기록이 1건이라도 있으면 자격 있음」이었으나 DD-21a 규칙 4 가 미실행을 막으므로 ①~④ 를 전수 실행한 회차만 완료 자격을 얻는다. 기존 테스트가 통과 1건짜리 로그를 쓰고 있어 함께 고쳤다.

실행은 OrchestratingVerificationRunner 가 오케스트레이션만 한다. 다섯 방법은 VerificationMethodExecutor 계약 뒤에 있어 판정 규칙이 실행 기반 없이 검증된다. 순서는 ④ → ② → ① → ③ 이고 실패 를 본 순간만 루프를 끊는다 — 통과 에는 멈추지 않는다(초판 규칙 철회). ①~④ 가 전부 대상없음 일 때만 ⑤ 를 연다: 하나라도 미실행이면 열지 않는다(미실행 ≠ 대상없음).

⑤ 의 파서는 내용을 인자로 받는다 — 파일을 읽지 않는다. 그래서 구현이 순수하고 (dart:io 없음) 파서 자체가 테스트되며, 읽기 실패가 파서 오류로 잘못 분류되지 않는다(읽기 실패는 failure:env_tool_aborted — D-055 §1 경계 사례).

⚠️ parseString 의 throwIfDiagnostics 를 false 로 내리면 어떤 입력도 통과한다 — 요건 P-2 가 깨지고 ⑤ 가 고무도장이 된다. 그 함정을 파서 구현에 주석으로 고정했다.

⚠️ 계약 스텁이 예고대로 갈라졌다. contract_registry_test.dart 의 12종 공용 스텁은 「계약들이 메서드를 갖게 되면 더 이상 컴파일되지 않는다 … 그 실패가 표면이 채워졌다는 신호다」라고 적혀 있었고, VerificationRunner.run 과 ProcessLauncher.run 이 같은 이름· 다른 시그니처가 되며 실제로 그 신호가 왔다. 예고대로 전용 스텁으로 나눴다.

DD-23c — 진단 원문 비영속의 귀결 4건 (#32). DD-23b 는 원문을 .cocode/history/ 에 적지 않는다고만 정하고 네 자리를 비워 두었다 — ⓐ "UI 세션 버퍼"가 무엇인지(보유 주체·수명·상한 초과 동작 — docs/ 전체에서 이 낱말의 정의는 0건이었다) ⓑ 종결된 작업의 검증 실패 원문을 나중에 볼 수 있는지 ⓒ verification_log 가 재시도 회차를 어떻게 담는지(위 스키마에 회차 식별자가 없었다) ⓓ 보존 정책 P4 의 "통째로 삭제"에 검증 기록이 들어가는지. 이 결정은 DD-23b 를 뒤집지 않고 그 네 자리를 채운다. 원문을 어떤 형태로든 영속하는 쪽으로 DD-23b 를 되돌릴 권한이 누구에게 있는가는 이 문서가 정하지 않는다(§9 미결 21, 일지 D-041).

① 세션 버퍼의 정의. "세션"은 AgentSession(§3.1 — 작업 1건의 수명)이 아니라 워크스페이스가 cocode ADE 프로세스에 열려 있는 구간이다. 그래서 아래에서는 워크스페이스 세션 버퍼라 부른다.

항 규칙 근거
ⓐ 보유 주체 toolchain(L1) 의 VerificationRunner 구현이 cocode ADE 프로세스 메모리에 워크스페이스별로 보유한다. 자식 프로세스의 stdout·stderr 를 받아 개수·exit code 로 접는 곳이 바로 여기이므로 원문이 처음 존재하는 자리이기도 하다. 읽기는 기존 VerificationRunner 계약에 읽기 전용 접근자(작업·회차·방법 → 원문 스트림)를 두어 cocode_app 이 소비한다 — 계약 12종 목록(§2.2, DD-02a)은 늘리지 않는다. ui(L0)는 원문을 뷰 모델 문자열로만 받고(C4), 고빈도 출력이므로 BlocSignal 을 통과시키지 않는다(DD-06 — terminal 출력과 같은 취급) 비영속이 규율이 아니라 구조로 강제된다. DD-15 의 dart:io 쓰기 허용 파일 목록은 workspace 저장소·게이트와 bricks GeneratorTarget 뿐이라, toolchain 안의 버퍼가 파일로 새는 경로는 그 가드(.github/scripts/check_*.py)가 막는다. 자식 프로세스 자신은 후보가 아니다 — 종료와 함께 사라지고, ③·⑤ 는 자식 프로세스가 아니다
ⓑ 수명 종료 조건 버퍼가 비는 사건은 둘뿐이다 — (1) 프로세스 종료(앱 재시작 — 메모리이므로 정의상) (2) 워크스페이스 닫힘(전환 포함 — 버퍼는 .cocode/ 처럼 워크스페이스 단위로 귀속된다). 작업 종결(완료·실패·롤백됨·거부됨·중지됨)은 버퍼를 비우지 않는다 — 그래야 P1 의 "돌아온 사람"이 같은 세션 안에서 실패 원문을 볼 수 있고(§4.4 실패 배너·완료승인 카드는 종결·대기 중 작업에 붙는다), ⓒ 의 정리 단위 "가장 오래 종결된 작업"이 성립한다 AC-19(편집은 종결 뒤에도 남는다)와 같은 방향. DD-13 트리거 3(앱 재기동 시 잔존 리스 전부 무효)과 같은 논리 — 프로세스 수명에 종속된 상태는 재기동 시 존재할 수 없다
ⓒ 상한 초과 시 동작 상한은 워크스페이스 세션 버퍼 총량 1값(바이트)만 둔다. 초과 시 정리는 판정 없이 세 단계로 내려간다 — (1) 가장 오래 종결된 작업의 버퍼부터 통째로 비운다(P1·P2·P4 와 같은 단위·순서 — 종결되지 않은 작업의 버퍼는 비우지 않는다. 종결 순서는 버퍼가 자기 시계로 기록하므로 작업 레코드에 시각 속성이 없어도 성립한다 — flow-permutation §8 R-4 가 지적한 함정을 피한다) (2) 종결된 작업의 버퍼를 다 비워도 넘으면, 남은 작업 안에서 attempt_no 가 가장 작은 회차의 스트림부터 비운다(현재 회차는 마지막까지 남는다) (3) 단일 스트림 하나가 상한을 넘으면 그 스트림의 앞부분을 잘라내고 꼬리를 남긴다(테스트·분석 출력은 요약이 끝에 온다). 어느 단계든 비워진 자리는 원문 대신 고정 문구("원문 버퍼 정리됨 — 세션 상한", 잘린 스트림은 "앞부분 N 바이트 생략")로 표시되고 조용히 사라지지 않는다 — 비움 사실은 버퍼가 (작업, 회차) 단위 묘비(tombstone) 로 세션 안에 기억한다 값은 정하지 않는다(근거 없음 — D-013·D-015·D-020 이 세 번 경계한 패턴). 무엇을 재야 하는가와 미정 시 동작은 §9 미결 20. 정리 순서를 "오래 종결된 작업 → 오래된 회차 → 스트림 앞부분"으로 둔 이유는 세 단계 모두 시각·번호 비교뿐이라 판정이 없고, 사용자가 잃는 것이 "지금 보고 있을 가능성이 가장 낮은 것"부터이기 때문이다

② 종결된 작업의 검증 실패 원문 — 사후 조회 경로를 신설하지 않고 "조회 불가"로 확정한다(선택지 ⓑ 채택). 종결 상태(완료·실패·롤백됨·거부됨·중지됨)에 도달한 작업의 검증 원문은 ①의 버퍼가 살아 있는 동안만 볼 수 있고, 프로세스 종료·워크스페이스 닫힘 뒤에는 어떤 경로로도 조회되지 않는다. 근거 둘을 함께 든다.

  • DD-23b 의 유출 표면 논거를 그대로 잇는다. DD-23b 가 원문을 배제한 근거는 "자유 텍스트 필드를 두지 않는다"였다 — 도구 출력에는 경로·환경·자격증명 흔적이 섞일 수 있고, .cocode/ 는 워크스페이스 안이라(DD-08) DD-09 가 거부하는 것은 쓰기뿐이어서 에이전트의 읽기는 막혀 있지 않으며, .cocode/ 를 VCS 에서 제외하는 규칙은 어느 문서에도 없다. 선택지 ⓐ(영속)는 마스킹·경로 제거 같은 "무엇이 비밀인가"의 판정을 도입해야 하고, 길이 상한은 비밀을 걸러 주지 않는다 — 이 문서가 일관되게 피해 온 "판정이 필요한 규칙"이다. 원문을 .cocode/ 밖(OS 앱 데이터 디렉터리)에 두는 변형도 DD-15 의 쓰기 허용 목록을 넓히고 유출 표면을 워크스페이스 밖으로 옮길 뿐 없애지 않는다.
  • UX 원칙 P1("화면은 돌아온 사람을 위해 설계한다" — ux-spec §1)은 원문 저장이 아니라 재현으로 충족한다. 돌아온 사람에게 남는 것은 ⓐ 편집 자체(AC-19 — 실패해도 조용히 폐기되지 않는다 · D-030 — 회차별로 누적된 채) ⓑ 회차별 verification_log(방법·결과·개수·exit code·no_target 사유코드 — ③) ⓒ 실패 사유 3종 + retry_count(ux-spec §3.2)다. 그리고 검증은 결정적이라고 전제되므로(state-machine §3 — "같은 입력으로 검증만 다시 돌리면 결과가 같다"), 남아 있는 편집 위에서 같은 방법(dart analyze·flutter test·빌드)을 사람이 여는 터미널(S-04)에서 다시 실행하면 원문은 재현된다 — DD-10 이 diff 를 저장하지 않고 base/result 에서 파생하는 것과 같은 구조다. 재현되지 않는 것은 ③(핫 리로드 런타임 오류)뿐인데, ③은 애초에 연결된 디버그 세션이 있을 때만 대상이 있고(DD-21) 그 세션 자체가 세션 종속이다. 이 문서는 "재검증 실행" 버튼 같은 새 기능을 요구하지 않는다 — 터미널이 이미 있다.

귀결 — 고지가 화면에 붙는다. 원문을 볼 수 있는 자리마다 "이 세션에서만" 고지가 있어야 P1 의 "돌아온 사람"이 다음 세션에 무엇이 없을지를 미리 안다. 고지 문구는 판정 없이 버퍼 상태 3종에 1:1 로 붙는 고정 문자열이다 — A 있음: "검증 원문은 이 세션에서만 볼 수 있습니다 — 워크스페이스를 닫거나 앱을 다시 열면 개수·종료 코드만 남습니다" · B 없음(기록 없음 = 이전 세션): "검증 원문은 이전 세션에 있었습니다 — 지금은 개수·종료 코드만 남아 있습니다. 같은 검증을 터미널에서 다시 실행하면 재현됩니다" · C 없음(①ⓒ 묘비): "검증 원문이 세션 버퍼 상한으로 정리되었습니다 — 개수·종료 코드만 남아 있습니다". 붙는 자리 세 곳(검증 로그 패널·실패 배너·완료승인 카드)과 [문제] 탭의 지위는 ux-spec §3.4·§4.4·§5.2·§10 S-08b(UX-D-22). Seed Spec §3(78행)은 verification_log 에 "수행한 자가 검증 방법과 결과"만 요구하고 원문을 요구하지 않으므로 이 채택은 Seed Spec 을 좁히지 않는다 → §10-18.

③ verification_log 는 회차별로 누적된다. 레코드는 (attempt_no, method) 당 1건이며 append-only 다 — 재시도가 앞 회차의 레코드를 덮어쓰거나 지우지 않는다. attempt_no 는 Seed Spec §3 EditSnapshot 의 attempt_no 와 같은 시계다(최초 0, retry_count 가 증가하는 ⑧ 전이 시점에 1 증가 — state-machine §3 · seed-spec §3 "시도 경계의 정의"). 스키마에는 위 JSON 의 attempt_no 한 필드가 는다. 단일 레코드(방법당 최신 결과만 유지)를 버린 이유: ⓐ Seed Spec 78행의 "수행한 방법과 결과"는 수행된 전부를 가리킨다 ⓑ D-030 이 편집을 회차별로 누적해 "표시해서 푼다"고 정했으므로 그 편집과 나란히 놓일 검증 결과도 같은 축을 가져야 B-15(모든 시도의 흔적이 diff 에 나타난다)와 대칭이 된다 ⓒ AC-19·P1 의 "돌아온 사람"에게 "3회차에 무엇이 실패했는가"는 편집만큼 중요한 정보다.

  • DD-21a 의 판정 집합: 규칙 1~4 는 현재 회차(attempt_no == retry_count)의 레코드만 본다(§6.1 에 같은 문장을 두었다). 회차를 가로질러 보면 한 번 실패한 작업은 영원히 완료할 수 없거나(실패 잔존) 검증을 건너뛰고 완료할 수 있게 된다(통과 잔존). 둘 다 결함이다.
  • 코드 영향(정직하게): 현행 core 의 VerificationEntry 에는 회차 필드가 없고 DelegatedTask.canCompleteBySelfVerification 은 로그 전체에서 통과를 찾는다(package/core/lib/src/entity/delegated_task.dart). 회차별 누적을 저장하는 순간 이 술어는 "앞 회차의 통과로 완료 자격이 생기는" 결함이 되므로, attempt_no 필드 추가와 술어의 현재 회차 한정은 같은 PR 에서 이뤄져야 한다. 소유 경계에 따라 그 PR 은 #63 이며 이 결정은 #63 의 입력이다.

④ 보존 정책 P4 의 "통째로"에 검증 기록은 들어가지 않는다. P2 의 삭제 단위는 작업의 EditSnapshot 집합(스냅샷 인덱스·버전 체인 항목·참조 blob)이고, 작업 레코드와 그 안의 verification_log 는 남는다. 이유 셋 — ⓐ P4 가 푸는 문제는 부피(CH-022 "스냅샷 볼륨")인데 verification_log 는 개수·코드뿐이라 부피가 없고, 작업 레코드 수는 사람이 만든 위임 수에 비례해 blob 처럼 자라지 않는다 ⓑ D-035 가 개정한 C-02 는 "삭제된 스냅샷의 경계 시점은 되돌림 대상이 아니며 그 사실은 사용자에게 고지된다"를 요구한다 — 고지가 붙을 작업이 목록에 남아 있어야 하고(ux-spec §4.5 R3: "[롤백] 이 비활성화되고 사유가 붙는다"), 작업 레코드를 지우면 고지할 대상이 사라진다 ⓒ DD-23b 의 유출 표면 논거는 verification_log 에 적용되지 않는다(원문이 없다). 귀결로 소실의 종류가 둘로 갈린다 — 롤백 지점 소실(P4·P4′ → P6 고지)과 검증 원문 소실(세션 종료 또는 ①ⓒ 정리 → ② 의 고지 B·C). 두 고지는 채널이 다르고 같은 문장에 섞지 않는다 — §4.4 의 P2·P4·P6 행에 그대로 적었다. P4 뒤 작업 레코드에 남는 "스냅샷 정리됨" 표시의 필드·형식은 #23(레코드)·#29(구현) 소유다.

소유 경계 (이 결정을 받는 쪽).

이슈 이 결정에서 받는 것
#63 검증 실행기·스키마 ③ attempt_no 필드 + canCompleteBySelfVerification 의 현재 회차 한정(같은 PR) · ①ⓐ 버퍼 보유(toolchain 메모리, 워크스페이스별) + VerificationRunner 읽기 전용 접근자 · ①ⓒ 정리 3단계와 묘비
#64 · #73 영향 없음 — no_target_code·result/failure_class 는 각자 소유. ③ 은 그 필드를 건드리지 않는다
#86 검증 로그 패널 ② 고지 3종(A/B/C) 문구 슬롯 + [문제] 탭 링크(ux-spec §3.4) · 원문 뷰 S-08b 의 작업·회차·방법 축
#29 보존 정책 구현 ④ — P4 는 스냅샷 집합만 지우고 작업 레코드·verification_log 를 남긴다 · P6 고지 종류는 롤백 지점 소실 2종 그대로(검증 기록 소실 종류는 추가되지 않는다)
#23 저장소 ④ 작업 레코드·verification_log 의 위치는 tasks/<task_id>/ 아래(§2.1 — 파일명은 #23) · P4 뒤 "스냅샷 정리됨" 표시 필드
#71 · #85 (중지·거부) ①ⓑ — 중지됨·거부됨도 종결 상태이므로 버퍼를 비우지 않는다
#17 상태 기계 구현 영향 없음 — retry_count 증가 지점(⑧)이 곧 attempt_no 증가 지점이라는 것을 ③ 이 재확인했을 뿐이다

이 결정이 정하지 않는 것.

  • 워크스페이스 세션 버퍼 총량 상한의 값 — 미정(§9 미결 20). 값 없이는 ①ⓒ 가 발동하지 않는다. 이 문서는 AC-08 과 같은 형식으로 "설치 직후 양의 유한한 기본값"의 존재 요건만 건다(Seed Spec 의 요구가 아니라 이 문서의 요구 — §10-19, §10-5 와 같은 성격).
  • DD-23b 의 보안 판단(원문을 어떤 형태로도 영속하지 않는다)을 유지·번복할 권한이 Design 에 있는가, 오너 확답이 필요한가 — 미정 — 결정 필요(판정 주체: 미정 → §9 미결 21 이 "누가·언제·미정 시 동작"을 채운다, 일지 D-041). ② 는 그 판단이 유지되는 동안의 귀결이며, 뒤집히면 ②·④ 와 ux-spec 의 고지 문구·bdd 의 비영속 시나리오가 재검토 대상이다. ①·③ 은 영향받지 않는다.
  • 사람이 여는 터미널(S-04)의 출력 버퍼는 이 결정의 대상이 아니다 — 그것은 terminal 의 것이고 §5.2 대로 집행 대상도 아니다.

ACP 에이전트의 자격증명은 cocode ADE 가 보관하지 않는다. ACP 에이전트는 자체 인증 흐름을 갖는다(planning-inputs §5.1: Claude Agent 는 auth terminal, Codex 는 auth agent). cocode ADE 는 그 흐름을 띄우는 UI 만 제공하며 토큰을 대신 저장하지 않는다. 흐름별 UI 처리는 PRD D-10(미결) 그대로다 → §9 미결 13.

7.2 ACP 에이전트 프로세스 권한 (DD-24)

  • 생성: toolchain 런처 경유(DD-15). 직접 spawn 경로 없음.
  • 네트워크: §5.3 프록시 강제. 에이전트가 접속하는 모델 API 호스트는 그 에이전트를 등록할 때 사용자 확인을 거쳐 allowed_hosts 사용자 추가분에 들어간다(seed-spec 70행).
  • 파일: 워크스페이스 쓰기는 클라이언트 fs 경유가 원칙(§3.3, §5.4). 자체 설정·캐시 경로는 Design 단계에서 각 에이전트를 실제로 띄워 확인해야 한다(planning-inputs §2.4 주, PRD D-7) → §9 미결 9. 그 경로들은 HOME 아래에 있으므로 DD-23a 의 HOME 통과가 전제 조건이다.
  • 통신: stdio 만 연결. 부모의 다른 파일 디스크립터를 상속시키지 않는다.
  • 종료: cancel() 이후 유예 시간 내 미종료면 강제 종료. 유예 값은 §9 미결 5 와 같은 취급.

7.3 C-05(테스트 보호)의 집행 조건

C-05·AC-11 의 경로 술어는 v3.1.0 이 확정했다 — test/·integration_test/·widgetbook/ 아래(seed-spec 64행). 게이트는 이 술어에 걸린 기존 파일에 대한 모든 쓰기를 승인 전까지 보류한다(파일 내용을 읽지 않고, 변경 성격을 분류하지 않는다 — PRD FR-403). 보류 중 작업은 pending_approval_kind=테스트변경승인 으로 사람승인대기 에 머문다(불변식 3). 단 이 보호도 §5.4 의 격리에 종속된다: 에이전트 프로세스가 게이트를 우회해 직접 쓰면 경로 술어가 작동하지 않는다. 즉 프로세스 격리 스파이크는 AC-14 뿐 아니라 C-05·C-02 의 집행 조건이기도 하다.


8. 이 설계가 내린 결정 (DD 목록)

ID 결정 근거의 성격
DD-00 패키지 신설 기준 K1(다중 소비자)·K2(교체 가능 외부 부품)·K3(계층 방향)·K4(집행 단일화/구현 격리) 초판 K1 이 자기 목록을 부정해 재정의
DD-01 12개 패키지 / 4계층(L0~L3) — desktop_platform 신설 포함 K1~K4 + package-layers + D-022
DD-02 순환 3곳·계층 위반 3곳·순방향 1곳을 계약+런타임 등록으로 차단 package-layers 88~101행
DD-02a 완결성 테스트 대상 = 계약 12종 전수 + 키 레지스트리 4종 + 라우트 인벤토리 package-layers 99~116행
DD-03 래칫 기준선 = 최대 SCC 1 · SCC 내부 엣지 0 · 유령 엣지 0 그린필드 + package-layers 157~162행
DD-04 엔티티 3종의 레이어 배치 flutter-patterns 41~43행 + Seed Spec §3
DD-05 status 전이는 도메인 함수 하나만 수행. pending_approval_kind 는 확장하지 않음 불변식 1~5 + §3 추가만 허용
DD-06 고빈도 UI 이벤트는 BlocSignal 을 통과시키지 않음(flutter-patterns 로부터의 의도적 이탈) AC-15 / A-03
DD-07 AgentRuntime/AgentSession + 알림/요청 분리, 요청은 응답 채널을 갖는다 오너 결정 1 + AC-06 ⓑ 실현
DD-08 / DD-08a .cocode/ 레이아웃 + path_key 정규화(구분자·NFC·볼륨별 대소문자 폴드) 오너 결정 3 + C-01 3플랫폼
DD-09 .cocode/** 에이전트 쓰기 항상 거부 AC-14 를 좁히는 안전 방향
DD-10 content-addressed blob + base/result 해시가 곧 주소 seed-spec 93·95행
DD-11 사전검사(ⓐ전역 마지막 스냅샷 소유 확인 + ⓑ해시 대조) → 저널 → 원자적 치환 → 재개 AC-02 + BDD 183행
DD-12 보존 정책 P1~P7 + P4′ 우선순위(구조 확정, 값 3개는 Planning 실측 후) CH-022 해소 + R-1/R-4
DD-13 잠금 리스 + 해제 트리거 3종 + edit_locks 초기값 = 빈 집합 AC-12·AC-18, PRD D-17 해소
DD-13a 스캐폴딩 원자성 = 스테이징 + 단일 디렉터리 커밋 AC-13, Workspace 불변식 3
DD-14 집행 지점은 프로세스 경계(인터셉트 아님) planning-inputs §2·§5.1
DD-15 단일 런처 + 허용 파일 목록 기반 custom_lint 판정 제거
DD-16 로컬 정책 프록시 AC-14 ⓑ
DD-17 리다이렉트 중간 호스트도 동일 규칙 판정 PRD 406행
DD-17a 미지 리다이렉트 대상 = 차단 + fail-closed + 사후 사용자 추가("승인 대기" 철회) pending_approval_kind 폐쇄 집합 · C-04 · AC-08 · PRD NFR-C
DD-18 허용 집합 = (WS ∪ write_exceptions) − 거부목록 PRD FR-701/D-19
DD-19 재지정 변수 없는 2경로는 실행 시 해석해 세션 고정 PRD D-20 해소 제안
DD-20 프로세스 파일 격리는 스파이크 종속, 그때까지 예방+검출 2단 한계 명시
DD-21 / 21a / 21b ①~④는 판정하지 않고 실행 · 실패 > 통과 우선순위 · ⑤ 자체의 실행 정의와 파서 레지스트리 PRD D-2·D-12 해소
DD-22 응답 불가 = S1(전송 종료) ∪ S2(유휴 초과) PRD D-3 해소
DD-23 / 23a / 23b 자격증명 OS 저장소 · HOME 포함 최소 화이트리스트 · verification_log 스키마(결과 포함, 원문 배제) 보안 + Seed Spec §3·AC-09
DD-23c 세션 버퍼 = toolchain 프로세스 메모리 · 워크스페이스 단위 수명 · 상한 정리 3단계 / 종결 작업의 검증 원문 사후 조회 불가 확정(재현으로 대체, 고지 A·B·C) / verification_log 회차별 누적(attempt_no, DD-21a 는 현재 회차만) / P4 삭제 단위에 작업 레코드·verification_log 불포함 #32 결정 · DD-23b 유지 · D-030·D-035 정합 · 값(버퍼 상한)은 §9 미결 20, 번복 권한은 §9 미결 21
DD-24 ACP 프로세스는 런처 경유·stdio 한정·최소 권한 보안
DD-25 생성·삭제·이름변경 = 부재값(∅) + previous_path 로 스냅샷 1건 표현, 불변식 2·4 는 경로별 투영 전이에 적용, 게이트 거부 3종, C-05 는 투영 전이 단위 판정 #28 결정 · I-4/FP-306·307 해소 · Seed Spec 문면은 개정 요청(§4.7, 일지 D-038)

9. 값이 없어 이 문서가 정하지 않은 것 (누가·언제)

# 항목 누가 언제 미정 시 동작
1 보존 정책 값(K·총량 상한 — 보존 기간은 D-039 로 제외) #29(값을 planning-inputs §7.2 로 옮겨 적는다). 산정 규칙·측정 규약·프록시 후보는 #30 이 확정(docs/retention-values-cocode.md, D-039) 후보는 지금 있다(unibook @ 9db9015 프록시). 실물 표본은 A-06 최소 셸 실측과 같은 회차에 SnapshotStore 에서 수집해 같은 규칙으로 재산정 #29 가 값을 옮겨 적기 전까지 삭제 트리거가 없어 무한 증가 — CH-022 의 원래 위험이 남는다
2 ~~AC-08 시도 시간 상한의 출하 기본값(AC-20 유휴 상한이 이것을 시드로 삼는다)~~ ✅ 해소 Planning(값 조정) / ~~오너 또는 Planning(최초 선택)~~ 최초 선택 완료 — 20분(오너 결정 2026-09-10, 일지 D-033, 결정 이슈 #106, planning-inputs §7.1). 근거는 job-timeout-budget B-1 을 CI 잡 실측 41건에 적용한 8.9m × 2 → 20분. AC-20 유휴 상한도 같은 값으로 파생된다 — 시드 규칙은 D-061(#50), 전문은 docs/unresponsive-detection-cocode.md §2. 남은 것은 #96 실측 후의 조정(Planning)뿐 ~~최초 선택이 없으면 AC-08·AC-20 둘 다 미충족~~ — 해소됨
3 ~~Windows 원자적 파일 치환 API 와 Dart 바인딩 동작~~ 해소(D-040, #27) — docs/windows-atomic-replace-cocode.md: Dart File.rename 그대로, FFI 미채택, 계약 W-1~W-7 Design → 확정 실측 완료(2026-09-10, windows-2022 러너 2회) 남은 미확정은 그 문서 §5(재시도 값·전원 차단 내구성·클라이언트 SKU·ReFS/SMB 등) — 값은 #25 가 파라미터로 둔다
5 ~~⑤의 형식별 정본 파서 등록 목록~~ ✅ 해소(D-054, #65) — .dart·.yaml/.yml·.json/.arb 3그룹(verification-parameters-cocode.md §2.2, 등록 요건 P-1·P-2·P-3). 구현은 toolchain 의 ToolchainFileFormatParserRegistry(#63). ⬜ 남은 축 3개: ③의 관찰 창(값 미정 — 실측 규약은 D-054 §1.3 으로 확정) · AgentRequest 타임아웃(형태만 해소 — #44 AgentRequestTimeouts, 종류별 4개·기본 생성자 없음. 나머지 상한과의 관계는 D-061(#50), 값은 미정) · 프로세스 종료 유예 Design 검증기·런타임 구현 시 ③ 세션 있을 때만 미실행 취급(차단 방향) / 미등록 형식은 ⑤도 대상없음 + no_target:no_registered_parser 기록(D-053)
6 플랫폼별 프로세스 격리 가용성 Design 스파이크 ✅ 부분 해소 — #38(sandbox-limits-cocode.md §2). macOS 가능(deprecated) · Windows 커널 객체 가능하나 FFI 필요 · Linux 는 CI 러너에서 불가(실사용자 데스크톱은 미측정). ⚠️ 「master 채널에서(D-022)」는 D-025 가 D-022 를 뒤집어 전제가 사라졌다 — stable 3.47.3 에서 측정했다 예방+검출 2단(DD-20), AC-14 부분 충족
7 ~~자격증명 저장 패키지 선정~~ ✅ 해소(D-064, #46) Design ~~구현 착수 시~~ — 착수와 함께 확정 pub 패키지를 쓰지 않는다 — 세 OS 의 공식 도구/API 를 직접 부른다(macOS /usr/bin/security · Linux secret-tool · Windows advapi32.dll FFI). 구현은 toolchain 의 OsCredentialVault
8 비정상 종료한 작업 자신의 status Design AC-18 구현 시 Seed Spec 미규정(PRD D-13). 잠금 해제는 DD-13 트리거로 이미 성립
9 ~~ACP 4종의 설정·캐시 경로~~ ✅ 해소(#48) Design 실기동 확인 완료 — 2026-09-13, macOS, acp/tool/acp_probe.dart ~/.claude·~/.codex·~/.gemini·~/.copilot·~/.npm/_npx 5종이 SandboxDefaults.writePaths 에 들어갔다(전부 HOME 상대 — DD-23a 의 HOME 통과가 전제). 전문: acp-agent-verification-cocode.md §5. ⚠️ macOS 1회차 한계 — Windows·Linux 경로는 자리표시이며 3-OS 회차에서 실측으로 바뀌어야 한다. 차단 방향은 그대로
11 ~~허용 호스트 개수 정본(11 vs 12)~~ 해소(D-051, #39) — 개수를 정본화하지 않는다. docs/planning-inputs-cocode.md §2.1 표(= SandboxDefaults.hosts)가 단일 SSOT 이고 개수는 거기서 파생되는 값이다 Planning → 확정(오너 결정 #109, 2026-09-13) 판정 완료 남은 영구 규칙: 어떤 문서도 개수를 판정 기준으로 인용하지 않는다 — 판정은 「비어 있지 않다」 + 대조 검사(#36). 표↔코드 행 집합 일치는 host_list_ssot_test.dart 가 고정한다
13 ~~ACP 인증 흐름 UI · 능력 차이 graceful degradation · v2 라우팅 seam 지점~~ ✅ 해소(D-062·D-063, #51) Design ~~acp 구현 시~~ — 구현 전에 닫았다(#47 이 이 결정을 소비한다) 전문: acp-integration-design-cocode.md. seam = 협상 직후 와이어 어댑터 선택 한 지점(v1 고정·!= 1 거부) · 인증은 terminal·agent 둘 다, 완료 관측은 프로토콜로만 · 능력 차이는 비활성 + 사유(자동 대체 없음, G-1~G-5)
14 ~~"파일의 편집을 마치는 시점" 판정 기준~~ 해소(D-045, #76) — docs/edit-lock-lifecycle-cocode.md: 획득 = 편집 시도 시점(후보 ⓐ) · 해제 = 게이트 연산의 반환(후보 ③ 도구 호출 반환) Design → 확정 2026-09-12 임시 규칙(「트리거 1·2로만 해제」)은 폐기하되 트리거 3종은 그대로 — 정상 경로가 리스 구간 안의 사망을 덮지 못한다. 남은 미결은 그 문서 §6(편집 스팬 배타성·읽기-수정-쓰기 창)
16 ~~"타겟하는 플랫폼"의 판정 입력~~ 해소(D-052, #39) — 워크스페이스의 플랫폼 디렉터리 존재로 판정한다(루트 바로 아래 · 이름 정확 일치 · 하나도 없으면 빈 집합) Design → 확정(오너 결정 #109, 2026-09-13) 판정 완료 PRD D-21 해소. 전 플랫폼 합집합 기본값이 없어져 AC-14 가 조인다. 규칙은 core 의 BuildTargetDetection, 디스크 판정은 workspace 의 WorkspaceBuildTargets. 사용자 설정 덮어쓰기는 범위 밖(필요 시 후속 이슈)
17 dart_sdk_mode 값 집합, agent_runtime_config 구조 Design 구현 착수 시 PRD §3.4 그대로
18 cob brick 이 데스크톱 IDE 형태를 지원하는지 Scaffold cob list-features 확인 D-023 미결. 새 brick 필요 가능성
19 게이트를 거치지 않은 삭제·이름변경(터미널 rm/mv — ACP v1 fs 능력에 삭제·이름변경이 없다)을 감시자가 검출했을 때의 표시·처분 Design #22·#37 구현 시 §10-② 근사 그대로 — 그 변경은 EditSnapshot 이 없어 롤백 대상이 아니며 검출 사실만 남긴다. DD-25(§4.7)는 게이트 경유 연산만 규정한다
20 워크스페이스 세션 버퍼 총량 상한의 값(DD-23c-①ⓒ — 값이 없으면 정리 3단계가 발동하지 않는다) Design(#63 검증 실행기 구현 시 — #86 패널이 소비) 참조 워크스페이스(unibook, SHA 고정 — D-019)에서 C-04 ①②④ 각각을 통과 1회·실패 1회 실행해 stdout+stderr 바이트를 재고(R-1 필드 동반), #30 의 산정 규칙 형식(실측 최대 × 2 · 과대 쪽이 안전측 — D-033 선례)으로 도출한 뒤 상한이 없는 동안 버퍼는 세션 종료까지 자란다 — 메모리 위험만 있고 영속 위험은 0(세션과 함께 사라진다). 출시 전 최초 선택 필수(AC-08 형식 유비 — 이 문서의 요구, §10-19)
21 DD-23b 의 보안 판단(원문을 어떤 형태로도 영속하지 않는다)을 유지·번복할 권한이 Design 에 있는가, 오너 확답이 필요한가(#32 AC 8) 프로젝트 오너 — 권한 배분은 오너만 정할 수 있다(일지 D-041 결정 대기) #63·#86 착수 전이 바람직하나 선행 조건은 아니다 — 판정이 늦어도 비영속 쪽은 재작업이 없다(부담의 부재) DD-23b·DD-23c ② 유지(차단 방향 — 영속하지 않는 쪽이 유출 표면이 작다). 오너가 "영속"으로 뒤집으면 DD-23c ②·④ 와 ux-spec 고지 문구·bdd 비영속 시나리오가 재검토 대상
22 해제 트리거 1·2 가 진행 중인 리스 구간을 멈추지 못한다 — 트리거가 구간 한가운데 발화하면 그 경로는 다음 대기자에게 넘어가는데 원래 구간의 게이트 연산은 계속 돌아, 두 작업이 같은 파일을 쓰는 창이 열린다(#75 코드 리뷰가 실측 재현: 트리거 직후 두 작업이 같은 경로 구간 안). 레지스트리 상태는 어긋나지 않지만(회수는 자기 리스만 지운다) 불변식 2 가 없애려는 그 상황이다 Design — ✅ #71 의 선행은 섰다(D-059): CancelReason 폐쇄 5종(§3.1 결정 주석)과 호출 경로(stop-and-limits-cocode.md §4.2). 남은 것은 EditLockRegistry.withLeases 에 취소 통로를 내는 레지스트리 계약 변경이다 지금 — 선행이 해소됐다 구현 결함이 아니라 계약의 공백이다 — EditLockRegistry.withLeases 에 취소 통로가 없다. 창의 크기는 게이트 연산 1회이며, 그 사이 두 쓰기는 게이트가 직렬화하므로 찢어진 상태가 되지는 않는다(직렬 순서로 겹쳐 쓸 뿐)
23 PendingChange(승인 대기 중 미적용 변경)의 저장 위치·수명·승인 후 EditSnapshot 승격 규칙 — #72 가 필요하다고 판정했다(FP-409 의 충돌 판정 입력인 「승인 시점 base」와 승인 시 쓸 제안 바이트를 들고 있어야 하고, 승인 대기에는 시간 상한이 없어(D-057 §2) 앱 재시작을 가로지르므로 영속이 필요하다). EditSnapshot 은 적용된 편집의 기록이라 이 둘을 담지 않는다 Design — 저장소 설계(#23 계열). ux-spec UX-D-19 가 이관한 자리다 #68·#69 구현 시 승인 후 적용이 재시작을 넘기지 못한다 — 카드는 남는데 적용할 것이 없다. 그동안 승인 대기는 세션 안에서만 유효하다

10. Seed Spec·상위 문서와의 어긋남 / 공백 (숨기지 않고 적는다)

  1. 작업 지시의 "Discovery 9분할"은 문서에 없다. Discovery 95~100행은 3분할이며, 9개 이름은 docs/ grep 히트 0건이다(§1.1, 재현 확인). 이 문서는 그 사실 위에서 패키지 경계를 새로 세웠다.
  2. C-02 × 외부 ACP 에이전트. 클라이언트 fs 능력을 무시하고 스스로 파일을 쓰는 ACP 에이전트가 있으면 "EditSnapshot 없는 편집 경로가 존재하지 않는다"(C-02)가 깨진다. 프로세스 격리가 확보되면 물리적으로 막히고, 아니면 감시자 검출이라는 근사로 남는다(§5.4). Seed Spec 은 이 경로를 다루지 않는다.
  3. AC-14 의 "차단"과 검출의 차이. 격리 확보 전까지 자식 프로세스의 워크스페이스 밖 쓰기는 예방이 아니라 사후 검출이다. AC-14 의 "위반 시도가 차단되는 것"을 완전히 만족한다고 주장하지 않는다(§5.4).
  4. 생성 파일·빌드 산출물의 지위가 미규정. build_runner 가 lib/ 아래에 만드는 .g.dart 는 에이전트가 촉발한 소스 트리 변경이지만, C-02 의 "에이전트가 생성한 파일 편집"에 해당하는지 Seed Spec 이 말하지 않는다. 이 문서는 게이트를 통과한 쓰기만 EditSnapshot 대상으로 두고 도구 산출물은 재생성 대상으로 표시하자고 제안하되 확정하지 않는다 — 확정 주체는 Planning(범위 문제이므로).
  5. AC-20 과 보존 정책에 기본값 요건이 없다. AC-08 은 "설치 직후 양의 유한한 기본값"을 강제하지만 AC-20 과 보존 정책에는 같은 요건이 없다. DD-22·DD-12 가 요건을 추가한 것은 이 문서의 결정이지 Seed Spec 의 요구가 아니다.
  6. 보존 정책의 참조값은 단위부터 우리 롤백 의미론과 어긋난다. 파일당 버전/용량 상한은 작업 단위 원자적 롤백(C-02)과 맞지 않는다(§4.4). ⚠️ rev.2 정정: 초판이 이 결론을 뒷받침하려 계산한 "10만 줄 ≈ 3.8MB" 는 근거가 지지하지 않는 유도였고(저장소 평균 비율의 역적용 · 존재하지 않는 픽스처 · 자기 설계의 gzip 무시), PRD R-4 의 재인용 금지와도 충돌해 삭제했다. 단위 불일치 논증만으로 결론은 그대로 선다.
  7. BDD U-10 이 낡았다. bdd 106행은 롤백 충돌 비교 대상을 "미정"으로 적지만, 같은 문서 머리말(15~22행)과 Seed Spec v3.1.0 이 이미 result_content_hash 로 확정했다. BDD 본문 갱신이 필요하다. 추가로 U-8·U-9 는 이 문서(§6.1·§6.2)가 해소했으므로 같은 표의 갱신 대상이다.
  8. flutter-patterns/SKILL.md 와 bloc-patterns.md 의 표기 불일치. 전자는 BLoC(26·42·44·46·47·128·132행)/BlocProvider(51행), 후자는 BlocSignal/BlocSignalProvider 다. 오너 결정 2 는 후자를 가리키므로 이 설계는 후자를 따랐다. ⚠️ 초판은 이 불일치의 행 번호를 "49·51행"으로 적었는데 49행은 CoUI 접두어 규칙이라 인용이 깨져 있었다 — 정정했다.
  9. Discovery §Technical Feasibility 의 UI 권장(material_ui/cupertino_ui 1.0, discovery 117행)을 채택하지 않았다. 오너 결정 4 와 조직 규약이 우선한다.
  10. MCP 는 §4 에 대응 AC 가 없다. Seed Spec 에서 MCP 는 §0 용어집(23행)에만 등장한다(전수 확인: 1회). 이 설계는 MCP 통합을 다루지 않으며, 범위 포함 여부는 Planning 미결이다.
  11. 식별자 충돌. 의사결정 일지의 D-001~D-023, PRD §4의 D-1~D-22, 이 문서의 DD-NN 이 공존한다. 후속 문서는 접두어와 출처 문서를 함께 적어야 하며, 일지의 마지막 번호를 매번 재확인해야 한다 — PRD 77행의 "D-001~D-021" 을 그대로 승계한 초판이 D-022·D-023 을 놓쳤다.
  12. C-04 ⑤의 세 번째 절이 공허해진다. ⑤는 ④가 file_not_analyzed 를 반환할 때만 열리므로, ⑤의 "정적 분석이 그 파일에 대해 새로운 오류를 보고하지 않음"은 그 상황에서 항상 참이다. Seed Spec 문면이 그렇게 되어 있는 결과이며 Design 이 좁힌 것이 아니다. Seed Spec 개정 시 검토 대상.
  13. AC-09 "1종 이상 통과"가 편집 대상을 검사한 방법일 것을 요구하지 않는다. 그 결과 편집과 무관한 방법(예: 기존 테스트 통과)이 완료를 열 수 있다(§6.1-6). Design 이 경계를 넓히지 않고 고칠 수 없으므로, A-02 스파이크의 ⓑ(에이전트 자신이 만든 결함 검출률) 설계에 이 시나리오를 반드시 포함할 것을 제안한다.
  14. Seed Spec 모호성 0.3125 는 v3.0.2 측정치다. v3.1.0 은 재평가 대기 상태이므로(seed-spec 8행), 이 문서를 포함한 후속 산출물은 v3.1.0 의 점수를 아는 상태에서 쓰인 것이 아니다.
  15. 🔄 해소됨 — #100/D-025 가 전제를 제거했다(상단 「D-022 재분류」). stable 을 유지하므로 master 채널 회귀를 감지할 대상이 없고, 대신 check_flutter_stable_pin.py 가 채널 이탈 자체를 막는다(#10). 아래는 master 를 채택했을 때의 서술로 보존한다. ~~D-022 가 인수한 위험은 이 문서가 해소하지 않는다.~~ master 채널은 제품 빌드 기반 전체에 적용되고 @internal API 는 패치 버전에서도 breaking change 가 예고돼 있다. desktop_platform 은 수정 지점을 한 곳으로 모을 뿐 위험을 줄이지 않는다. CI 가 master 채널 회귀를 매일 감지하는 장치가 별도로 필요하며, 그 설계는 Breakdown 사항이다.
  16. Seed Spec §3 이 두 해시를 "해당 파일 내용의 해시"로만 정의해 생성·삭제·이름변경을 표현할 수 없다. DD-25(§4.7)가 부재값·previous_path·경로별 투영으로 표현을 정했고, 문면 반영은 §4.7 «개정 요청»으로 오너에게 올렸다(의사결정 일지 D-038 결정 대기). 판정 전까지 이 설계는 D-036 이전의 #24 와 같은 "스펙보다 앞선" 상태다 — #23 착수 전에 판정이 필요하다.
  17. AC-11 의 "내용을 변경"이 삭제·이름변경을 덮는지 문면이 말하지 않는다. C-05 본문("모든 변경")은 덮는다고 읽히고 AC-11 은 좁다. DD-25 R6 은 C-05 를 따라 투영 전이 단위로 판정하며(테스트 파일 삭제·테스트 경로에서 나가는 이름변경 = 승인 대상, 생성만 예외), 문면 정합은 §4.7 개정 요청 ③ 이다.
  18. 검증 원문의 사후 조회 불가(DD-23c-②, #32)는 Seed Spec §3 을 좁히지 않는다. §3(78행)의 verification_log 는 "수행한 자가 검증 방법과 결과"만 요구하고 원문을 요구하지 않으며, AC-09·AC-19 도 원문을 요구하지 않는다 — Seed Spec 개정 요청 없음. 좁혀지는 것은 ux-spec 원칙 P1 의 적용 범위(원문에 한해 "돌아온 사람"은 다음 세션에서 재현으로 대신한다)이고, 그것은 Design 결정이다.
  19. 세션 버퍼 상한에 "설치 직후 양의 유한한 기본값" 요건을 건 것은 이 문서의 결정이다(DD-23c-①ⓒ, §9 미결 20) — §10-5 의 AC-20·보존 정책과 같은 성격이며 Seed Spec 의 요구가 아니다.
  20. seed-spec:70 이 edit_locks 를 「파일 경로 집합」으로 정의해 소유자를 표현할 수 없다. DD-13(§4.5)이 경로 → 리스(소유 task_id·소유 세션 식별자·갱신 시각)로 대체했고 #75 가 그대로 구현했다 — 해제 트리거 1(세션 종료)·2(작업 종결)가 소유자를 알아야 성립하기 때문이며, 집합으로는 그 둘이 구현 불가다. 문면 반영은 §4.5 «개정 요청»으로 오너에게 올렸다(의사결정 일지 D-047 판정 대기). ⚠️ 판정 전까지 이 설계는 §10-16(DD-25)이 D-038 판정 전에 있던 것과 같은 「스펙보다 앞선」 상태다. 다만 성질이 다르다 — DD-25 는 엔티티 개정이 선행하지 않으면 후속 Story 가 막혔지만, 여기서는 Workspace.editLocks 가 이미 Map<String, EditLease> 로 서 있고(#15) 그 위에서 AC-12·AC-18 이 성립하므로 막히는 후속 작업이 없다. 판정이 「집합 유지」로 뒤집히면 해제 트리거 1·2 를 다시 설계해야 한다.

Generated by cc-product Design stage (BMAD v6) · 2026-08-26