[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CBW2YCWMMEQV5WHMTCTPVD":3,"skill-same:01M6CBW2YCWMMEQV5WHMTCTPVD":56},{"skillNo":4,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":21,"linkOnly":22,"markdownContent":23,"markdownBody":23,"frontMatter":24,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":13,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":6,"ownerDescription":30,"updatedAt":31,"install":32,"resources":45},"01M6CBW2YCWMMEQV5WHMTCTPVD","33-protocol-spec",null,"devwinsoft\u002Fdevian","devwinsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F22570160?v=4","01M6C6RR8W05NH8NPKQNCE3Q72","maoshy","01M6C6RRDJ5TJJCZXRTY801YDH",0,"MIT","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F11-builder\u002F33-protocol-spec\u002FSKILL.md",[],[18,19,20],"spec","protocol","33 protocol spec","2026-11-10 10:12:30",false,"# 33-protocol-spec\n\nStatus: ACTIVE  \nAppliesTo: v10  \nSSOT: skills\u002Fdevian\u002F10-module\u002F03-ssot\u002FSKILL.md\n\n## Purpose\n\nPROTOCOL(DomainType=PROTOCOL) 입력으로부터 C#\u002FTS 프로토콜 코드를 생성하는 **전체 흐름**을 정의한다.\n\n이 문서는 **입력 포맷 \u002F 레지스트리(결정성) \u002F 경로 규약**만 규정한다.\n생성 코드의 구체적 API\u002F산출물은 **런타임\u002F제너레이터 코드**를 정답으로 본다.\n\n---\n\n## Inputs\n\n입력은 `{buildInputJson}`의 `protocols` 섹션(배열)이 정본이다.\n\n> `{buildInputJson}` 위치는 유동적이다. 예: `input\u002Fbuild_input.json`\n\n```json\n\"protocols\": [\n  {\n    \"group\": \"Game\",\n    \"protocolDir\": \".\u002FProtocols\u002FGame\",\n    \"protocolFiles\": [\"C2Game.json\", \"Game2C.json\"]\n  }\n]\n```\n\n- `group`: ProtocolGroup 이름 (C# 프로젝트명, TS 폴더명에 사용)\n- `protocolDir`: Protocol JSON 및 Registry 파일이 위치한 디렉토리\n- `protocolFiles`: 처리할 Protocol JSON 파일 목록\n- 파일명 base가 **ProtocolName**이 된다. (예: `C2Game.json` → `C2Game`)\n\n**금지 필드 (존재 시 빌드 실패):**\n- `csTargetDir` — `csConfig.generateDir` 사용\n- `tsTargetDir` — `tsConfig.generateDir` 사용\n- `upmName` — 금지 (Protocol은 `com.devian.foundation\u002FSamples~\u002F{ProtocolGroup}Protocol\u002F`에 샘플로 생성)\n\n> **normalize 규칙 (요약):** trim → 공백을 `_`로 치환 → 허용 문자 외 제거(영문\u002F숫자\u002F`_`\u002F`-`만 남김) → 소문자화. 정확한 규칙은 빌더 코드를 정답으로 본다.\n\n### Protocol Spec JSON (필수 필드)\n\n최소 구조:\n\n```json\n{\n  \"direction\": \"client_to_server | server_to_client | bidirectional\",\n  \"messages\": [\n    {\n      \"name\": \"MessageName\",\n      \"opcode\": 100,              \u002F\u002F optional\n      \"fields\": [\n        { \"name\": \"field\", \"type\": \"int32\", \"tag\": 1, \"optional\": true }\n      ]\n    }\n  ]\n}\n```\n\n추가 키가 존재할 수 있다. \"지원 여부\u002F정확한 스키마\"는 코드를 정답으로 본다.\n\n---\n\n## Determinism Gate (opcode \u002F tag)\n\nProtocol 호환성을 위해 Registry 파일을 사용한다.\n\n- `{ProtocolName}.opcodes.json`\n- `{ProtocolName}.tags.json`\n\nRegistry 파일은 `protocolDir\u002FGenerated\u002F`에 위치하며, 빌드 시 갱신된다.\nRegistry는 \"생성된 입력\" 파일로, 기계가 생성하지만 입력 폴더에 보존된다.\n\n정책:\n\n1) 명시 값 우선\n2) 레지스트리 값은 호환성 보존을 위해 유지\n3) 미지정 값은 **결정적 규칙으로 자동 할당**\n4) Tag의 reserved range(19000..19999) 금지\n\n> 자동 할당의 상세 규칙(최소값\u002F정렬\u002F증가)은 코드를 정답으로 본다.\n\n---\n\n## Outputs & Paths\n\n경로 규약은 SSOT를 따른다.\n\n**C# (ProtocolGroup = {ProtocolGroup}):**\n- staging: `{tempDir}\u002FDevian.Protocol.{ProtocolGroup}\u002Fcs\u002FGenerated\u002F{ProtocolName}.g.cs`\n- final: `{csConfig.generateDir}\u002FDevian.Protocol.{ProtocolGroup}\u002FGenerated\u002F{ProtocolName}.g.cs`\n- 프로젝트 파일: `{csConfig.generateDir}\u002FDevian.Protocol.{ProtocolGroup}\u002FDevian.Protocol.{ProtocolGroup}.csproj` (수기\u002F고정, 빌더가 생성\u002F수정 금지)\n- namespace: `Devian.Protocol.{ProtocolGroup}` (변경 금지)\n\n**TypeScript:**\n- staging: `{tempDir}\u002F{ProtocolGroup}\u002Fts\u002FGenerated\u002F{ProtocolName}.g.ts`\n- final: `{tsConfig.generateDir}\u002Fdevian-protocol-{protocolgroup}\u002FGenerated\u002F{ProtocolName}.g.ts`\n- `index.ts`는 모듈 루트에 존재하되 수기\u002F고정, 빌더가 생성\u002F수정 금지\n- 패키지명: `@devian\u002Fprotocol-{protocolgroup}`\n\n> **생성물 namespace 고정 (Hard Rule):**\n> C# 생성물 namespace는 `Devian.Protocol.{ProtocolGroup}`으로 고정이며, 런타임 모듈 단일화와 무관하게 변경하지 않는다.\n\n---\n\n## Networker \u002F SessionHost 생성 (Hard Rule)\n\n**프로토콜 코드젠은 Stub, Proxy, Runtime 외에 Networker와 SessionHost를 프로토콜별로 생성한다.**\n\n### Stub 변경 (기존 `.g.cs` 내부)\n\n| 항목 | 기존 | 변경 |\n|------|------|------|\n| 클래스 | `abstract class Stub` \u002F `protected` 생성자 | `sealed class Stub` \u002F `public` 생성자 |\n| 메시지 핸들링 | `protected abstract void OnXxx()` | `_handler.OnXxx()` (IHandler 위임) |\n| 확장 방식 | 상속 (override) | `IHandler` 구현 + `SetHandler()` 호출 |\n\n### IHandler 인터페이스\n\n각 `{Protocol}.g.cs`의 `static partial class {Protocol}` 내부에 `IHandler` 인터페이스를 생성한다.\n\n```csharp\n\u002F\u002F Game2C.g.cs 내부\npublic interface IHandler\n{\n    void OnPong(EnvelopeMeta meta, Pong message);\n    void OnEchoReply(EnvelopeMeta meta, EchoReply message);\n}\n\npublic sealed class Stub\n{\n    private IHandler _handler = null!;\n    public Stub(ICodec? codec = null) { ... }\n    public void SetHandler(IHandler handler) => _handler = handler;\n    public void Dispatch(PacketEnvelope envelope) { \u002F* _handler.OnXxx() 호출, unknown opcode → Log.Warn *\u002F }\n}\n```\n\n### {Protocol}SessionHost\n\n프로토콜별 세션 호스트. 기존 그룹 단위 `ClientSessionHost.g.cs`를 대체한다.\n교차 프로토콜 조합: `{Protocol}.Stub`(수신) + `{CounterProtocol}.Proxy`(송신).\n\n```csharp\n\u002F\u002F Game2CSessionHost.g.cs\npublic sealed class Game2CSessionHost : NetClientSessionHost\n{\n    public Game2CSessionHost(\n        Game2C.Stub stub,\n        C2Game.Proxy proxy,\n        INetConnector? connector = null)\n        : base(\n            () => new Game2C.Runtime(stub),\n            new INetSessionBindable[] { proxy },\n            connector)\n    { }\n}\n```\n\n### {Protocol}Networker\n\n프로토콜별 Networker (plain C#).\n\n```csharp\n\u002F\u002F Game2CNetworker.g.cs\npublic sealed class Game2CNetworker\n{\n    private readonly Game2C.Stub _stub;\n    private readonly C2Game.Proxy _proxy;\n    private readonly Game2CSessionHost _session;\n\n    public C2Game.Proxy Proxy => _proxy;\n\n    public Game2CNetworker(INetConnector? connector = null)\n    {\n        _stub = new Game2C.Stub();\n        _proxy = new C2Game.Proxy();\n        _session = new Game2CSessionHost(_stub, _proxy, connector);\n    }\n\n    public void SetHandler(Game2C.IHandler handler) => _stub.SetHandler(handler);\n\n    public void Connect(string url)\n    {\n        Log.Info($\"[Game2CNetworker] Connect: {url}\");\n        _session.Connect(url);\n    }\n\n    public void Tick() => _session.Tick();\n\n    public void Disconnect()\n    {\n        Log.Info(\"[Game2CNetworker] Disconnect\");\n        _session.Disconnect();\n    }\n}\n```\n\n### 교차 프로토콜 조합 규칙\n\n프로토콜 그룹 내에서 direction이 반대인 프로토콜끼리 페어링된다.\n\n```\nGame 그룹:\n  Game2C (server_to_client) ↔ C2Game (client_to_server)\n\n  Game2CNetworker  = Game2C.Stub  + C2Game.Proxy  (클라이언트용)\n  C2GameNetworker  = C2Game.Stub  + Game2C.Proxy  (서버용)\n```\n\n### 표준 사용 흐름\n\n```csharp\n\u002F\u002F 1. IHandler 구현\npublic class MyGameHandler : Game2C.IHandler\n{\n    public void OnPong(Game2C.EnvelopeMeta meta, Game2C.Pong message) { ... }\n    public void OnEchoReply(Game2C.EnvelopeMeta meta, Game2C.EchoReply message) { ... }\n}\n\n\u002F\u002F 2. 생성 + 핸들러 설정 + 연결\nvar networker = new Game2CNetworker();\nnetworker.SetHandler(new MyGameHandler());\nnetworker.Connect(\"ws:\u002F\u002Flocalhost:8080\");\n\n\u002F\u002F 3. 매 프레임\nnetworker.Tick();\n\n\u002F\u002F 4. 송신\nnetworker.Proxy.SendPing(new C2Game.Ping { Timestamp = now });\n\n\u002F\u002F 5. 정리\nnetworker.Disconnect();\n```\n\n### SessionHost 연결 규칙 (Hard Rule)\n\n**Generated Proxy는 연결 수명주기를 다시 가지면 안 된다. 연결\u002F정리\u002F이벤트\u002Fstate는 Networker\u002FSessionHost 한 곳에만 둔다.**\n\n**핵심 규칙:**\n1. generated `Proxy` public API는 `AttachSession()`, `DetachSession()`, `SendXxx()`로 설명 가능해야 한다\n2. Networker\u002FSessionHost가 paired inbound runtime 생성, `INetSession` 생성, 이벤트 구독, `AttachSession()` 호출을 맡는다\n3. 세션 해제 시 Networker\u002FSessionHost가 먼저 이벤트를 끊고 `DetachSession()`으로 stale binding을 제거한다\n\n### OnError 디듀프 가드 (Hard Rule)\n\n**`NetClientSessionHost`는 연결 실패 시 OnError를 Attempt당 최대 1회만 발생시킨다(디듀프 가드).**\n\n> 이 로직은 foundation 수기 코드(`NetClientSessionHost`)에 구현되어 있다. Generated Proxy는 send-only이며 에러 가드를 갖지 않는다.\n\n**핵심 규칙:**\n1. 한 연결 시도에서 OnError는 최대 1회만 Invoke\n2. _errorNotified가 true이면 추가 에러 무시\n3. 새 연결\u002F연결 성공\u002F세션 정리 시 플래그 리셋\n4. stale session 이벤트는 `ReferenceEquals` 가드로 무시\n\n---\n\n## UPM 산출물 정책 (Hard Rule)\n\n**Protocol Sample(`Samples~\u002F{ProtocolGroup}Protocol\u002F`)은 Runtime-only이며, 빌더가 touch 가능한 범위는 `Runtime\u002FGenerated\u002F`뿐이다.**\n\n| 대상 | 빌더 동작 |\n|-----------|----------|\n| `Runtime\u002FDevian.Samples.{ProtocolGroup}Protocol.asmdef` | 수기 파일 (빌더 수정 금지) |\n| `Runtime\u002FGenerated\u002F{ProtocolName}.g.cs` | ✅ 생성\u002F갱신 |\n| `Runtime\u002FGenerated\u002F{Protocol}SessionHost.g.cs` | ✅ 생성\u002F갱신 |\n| `Runtime\u002FGenerated\u002F{Protocol}Networker.g.cs` | ✅ 생성\u002F갱신 |\n| ~~`Runtime\u002FGenerated\u002FClientSessionHost.g.cs`~~ | ❌ 삭제 (프로토콜별 SessionHost로 대체) |\n| `README.md` | 수기 파일 (빌더 수정 금지) |\n| `Editor\u002F` 폴더 | ❌ 생성 금지, 존재 시 레거시 청소로 삭제 |\n\n**Runtime asmdef references 정책:**\n- `Devian.Core` (com.devian.foundation)\n- `Devian.Samples.{CommonDomainKey}Package` (com.devian.foundation — `getSampleName('Common', 'Package')`)\n\n> SSOT: `skills\u002Fdevian\u002F80-tools\u002F11-builder\u002F03-ssot\u002FSKILL.md` — Protocol UPM 산출물 정책\n\n---\n\n## Unity Compatibility (Hard Rule)\n\nUnity 환경에서의 호환성을 위해 다음 규칙을 강제한다.\n\n**C# Protocol 코드 생성 시:**\n\n1. **System.Text.Json 사용 금지**\n   - Unity는 `System.Text.Json`을 기본 제공하지 않음\n   - `using System.Text.Json;` 생성 금지\n   - `JsonSerializer`, `JsonSerializerOptions` 등 사용 금지\n\n2. **CodecJson 생성 금지**\n   - JSON 코덱은 생성하지 않음\n   - `CodecProtobuf`만 생성 (기본 코덱)\n   - Stub 생성자 기본값도 `CodecProtobuf` 사용\n\n3. **ICodec 인터페이스**\n   - 인터페이스는 유지 (확장성)\n   - 기본 구현체는 `CodecProtobuf`만 제공\n\n---\n\n## Protocol 모듈 의존성 (Hard Rule)\n\n**C# PROTOCOL 모듈 의존성:**\n- `Devian.Protocol.{ProtocolGroup}.csproj`는 다음을 ProjectReference 한다:\n  - `..\\Devian\\Devian.csproj`\n  - `..\\Devian.Domain.Common\\Devian.Domain.Common.csproj`\n- 각 생성물(`{ProtocolName}.g.cs`)은 `using Devian;`을 포함해야 한다. (namespace는 Devian 단일)\n\n**TS PROTOCOL 패키지 의존성:**\n- `@devian\u002Fprotocol-{protocolgroup}`는 `@devian\u002Fcore` + `@devian\u002Fmodule-common`을 의존한다.\n\n**Unity UPM:**\n- Protocol Sample의 `.asmdef` 파일의 `references`에 `Devian.Samples.{CommonDomainKey}Package` 포함 필수\n- 예: `Devian.Samples.{ProtocolGroup}Protocol.asmdef` → `\"references\": [..., \"Devian.Samples.CommonPackage\"]`\n\n---\n\n## TypeScript Namespace 규칙\n\nTS 생성물은 **ProtocolName 단위**로 네임스페이스가 생성된다.\n\n**생성 형태:**\n```typescript\n\u002F\u002F {ProtocolName}.g.ts\nexport namespace {ProtocolName} {\n    export interface MessageName { ... }\n    export const Opcodes = { ... } as const;\n}\n```\n\n**핵심 규칙:**\n1. `.g.ts` 파일은 `export namespace {ProtocolName}`만 생성\n2. `index.ts`에서 Direct export 제공\n3. 소비자 코드는 Direct import를 사용\n\n**생성 예시 (index.ts):**\n```typescript\nimport * as C2GameMod from '.\u002FC2Game.g';\nimport * as Game2CMod from '.\u002FGame2C.g';\n\nexport const C2Game = C2GameMod.C2Game;\nexport const Game2C = Game2CMod.Game2C;\n\nexport { createServerRuntime } from '.\u002FGenerated\u002FServerRuntime.g';\nexport { createClientRuntime } from '.\u002FGenerated\u002FClientRuntime.g';\n```\n\n**사용법 (권장):**\n```typescript\nimport { C2Game, Game2C, createClientRuntime } from '@devian\u002Fprotocol-game';\n\n\u002F\u002F 타입 사용\nconst req: C2Game.LoginRequest = { ... };\nconst ack: Game2C.LoginAck = { ... };\n\n\u002F\u002F Opcode 사용\nconst opcode = C2Game.Opcodes.LoginRequest;\n```\n\n---\n\n## ServerRuntime \u002F ClientRuntime 생성 (TypeScript)\n\nProtocol 그룹에 inbound와 outbound가 **정확히 1개씩** 존재하면 Runtime을 자동 생성한다.\n\n**ServerRuntime (서버 관점):**\n- inbound: client_to_server (예: C2Game)\n- outbound: server_to_client (예: Game2C)\n\n**ClientRuntime (클라이언트 관점):**\n- inbound: server_to_client (예: Game2C)\n- outbound: client_to_server (예: C2Game)\n\n**생성 조건:**\n- inbound 1개 + outbound 1개 → 생성\n- bidirectional만 존재 → 생성 안함 (정상)\n- 그 외 (0개, 2개 이상, 한쪽만 존재) → **빌드 에러**\n\n**생성 파일:**\n- `{tsConfig.generateDir}\u002Fdevian-protocol-{group}\u002FGenerated\u002FServerRuntime.g.ts`\n- `{tsConfig.generateDir}\u002Fdevian-protocol-{group}\u002FGenerated\u002FClientRuntime.g.ts`\n\n---\n\n## TypeScript package.json (생성 산출물)\n\n`devian-protocol-*` 패키지의 `package.json`은 **빌드 시스템이 생성하는 산출물**이다.\n\n**수정 금지 정책:**\n- 수동 편집 금지\n- 빌드 시 덮어쓰기됨\n\n**생성 내용:**\n- `name`: `@devian\u002Fprotocol-{group}`\n- `exports`: `.` + Runtime 존재 시 `.\u002Fserver-runtime`, `.\u002Fclient-runtime`\n- `dependencies`: `@devian\u002Fcore`\n\n> 위 dependencies 목록에는 **항상** `@devian\u002Fmodule-common`이 포함되어야 한다. (참조 판정 없음)\n\n---\n\n## Implementation Reference (정본 위치)\n\n**구현 정본 파일:**\n\n| 파일 | 함수 | 역할 |\n|------|------|------|\n| `framework-ts\u002Ftools\u002Fbuilder\u002Fbuild.js` | `generateCsproj(...)` | C# csproj 생성\u002F보정 (ProtocolGroup 포함) |\n| `framework-ts\u002Ftools\u002Fbuilder\u002Fbuild.js` | `ensureProtocolPackageJson(...)` | TS package.json 생성\u002F보정 |\n| `framework-ts\u002Ftools\u002Fbuilder\u002Fgenerators\u002Fprotocol-cs.js` | `generateCSharpProtocol(...)` | C# `{ProtocolName}.g.cs` 생성 |\n| `framework-ts\u002Ftools\u002Fbuilder\u002Fgenerators\u002Fprotocol-cs.js` | `generateCSharpSessionHost(...)` | C# `{Protocol}SessionHost.g.cs` 생성 |\n| `framework-ts\u002Ftools\u002Fbuilder\u002Fgenerators\u002Fprotocol-cs.js` | `generateCSharpNetworker(...)` | C# `{Protocol}Networker.g.cs` 생성 |\n\n**의존성 Hard Rule이 실제로 강제되는 지점:**\n\n- **C#:**\n  - csproj: `generateCsproj(...)`가 `Devian.csproj` + `Devian.Domain.Common.csproj` ProjectReference 포함\n  - g.cs: `generateCSharpProtocol(...)`가 `using Devian;` 포함\n- **TypeScript:**\n  - package.json: `ensureProtocolPackageJson(...)`가 dependencies에 `@devian\u002Fcore` + `@devian\u002Fmodule-common` 포함\n\n---\n\n## Verification Checklist (Hard)\n\n빌드 후 반드시 확인해야 하는 사항:\n\n**C#:**\n\n1. 생성된 `framework-cs\u002Fmodule\u002FDevian.Protocol.{ProtocolGroup}\u002FDevian.Protocol.{ProtocolGroup}.csproj`에\n   `..\\Devian\\Devian.csproj` + `..\\Devian.Domain.Common\\Devian.Domain.Common.csproj` ProjectReference 존재\n\n2. 생성된 `framework-cs\u002Fmodule\u002FDevian.Protocol.{ProtocolGroup}\u002F{ProtocolName}.g.cs` 상단에\n   `using Devian;` 존재\n\n3. 생성된 `{ProtocolName}.g.cs`에 `System.Text.Json` 관련 코드 없음\n\n4. 생성된 `{ProtocolName}.g.cs`에 `CodecJson` 클래스 없음\n\n**TypeScript:**\n\n5. 생성된 `framework-ts\u002Fmodule\u002Fdevian-protocol-{group}\u002Fpackage.json` dependencies에  \n   `@devian\u002Fmodule-common` 존재\n\n**Unity UPM:**\n\n6. Protocol Sample의 `Devian.Samples.{ProtocolGroup}Protocol.asmdef`의 `references`에 `Devian.Samples.CommonPackage` 존재\n\n---\n\n## Protocol Message Pooling\n\n생성된 프로토콜 코드는 메시지 객체 풀링을 지원한다. 풀링은 **새 API**로 제공되며, 기존 `Decode\u003CT>()`\u002F`Decode(opcode)`는 호환성을 위해 그대로 유지된다.\n\n### 생성 API (필수)\n\n각 `{ProtocolName}.g.cs`의 `public static partial class {ProtocolName}` 내부에 아래 API가 생성되어야 한다:\n\n```csharp\n\u002F\u002F 풀 필드 (메시지 타입별, max=256)\nprivate static readonly PacketPool\u003CFoo> _pool_Foo = new PacketPool\u003CFoo>(256);\n\n\u002F\u002F opcode 기반 풀링 디코드\npublic static object? RentDecodePooled(int opcode, ReadOnlySpan\u003Cbyte> data);\n\n\u002F\u002F 제네릭 풀링 디코드\npublic static T RentDecodePooled\u003CT>(ReadOnlySpan\u003Cbyte> data) where T : class, new();\n\n\u002F\u002F 반환 (타입 패턴 매칭)\npublic static void ReturnPooled(object message);\n\n\u002F\u002F 제네릭 반환\npublic static void ReturnPooled\u003CT>(T message) where T : class;\n```\n\n### Reset 규약 (필수)\n\n풀에서 꺼낸 메시지는 디코드 전에 반드시 `_Reset()` 호출해야 한다.\n\n**List\u003CT>? 필드의 Reset 처리:**\n- `null`로 버리지 말고 `Clear()` 호출\n- 생성 형태: `if (Field != null) Field.Clear();`\n\n### 기존 API 호환성\n\n- `CodecProtobuf.Decode\u003CT>()`, `Decode(opcode)`는 **그대로 유지**\n- 풀링은 새 API(`RentDecodePooled`\u002F`ReturnPooled`)로만 제공\n- 기존 사용자 코드 파괴 금지\n\n### DoD (완료 정의)\n\n- [ ] 생성된 `.g.cs`에서 `PacketPool\u003CT>`가 \"정의만\" 되지 않고, `RentDecodePooled`\u002F`ReturnPooled`에서 실제로 사용됨\n- [ ] `_Reset()` 메서드에서 `List\u003CT>` 필드는 `?.Clear()` 형태로 리셋됨\n- [ ] 기존 `Decode\u003CT>()`\u002F`Decode(opcode)`는 그대로 유지됨 (호환성)\n- [ ] `ReturnPooled(object)`는 패턴 매칭으로 타입 분기하며, `default`는 조용히 무시\n\n---\n\n## Zero-Alloc Encoding (Hard Rule)\n\n**프로토콜 인코딩은 IBufferWriter\u003Cbyte> 기반으로 구현되어 GC 할당이 발생하지 않는다.**\n\n### ICodec 인터페이스\n\n```csharp\npublic interface ICodec\n{\n    \u002F\u002F\u002F \u003Csummary>Encode to byte array (legacy, avoid in hot path)\u003C\u002Fsummary>\n    byte[] Encode\u003CT>(T message) where T : class;\n    \u002F\u002F\u002F \u003Csummary>Encode directly to IBufferWriter (zero-alloc send path)\u003C\u002Fsummary>\n    void EncodeTo\u003CT>(IBufferWriter\u003Cbyte> writer, T message) where T : class;\n    T Decode\u003CT>(ReadOnlySpan\u003Cbyte> data) where T : class, new();\n    object? Decode(int opcode, ReadOnlySpan\u003Cbyte> data);\n}\n```\n\n### PooledBufferWriter\n\n`Devian.PooledBufferWriter`는 ArrayPool 기반 IBufferWriter 구현체이다:\n\n```csharp\npublic sealed class PooledBufferWriter : IBufferWriter\u003Cbyte>, IDisposable\n{\n    \u002F\u002F ArrayPool\u003Cbyte>.Shared.Rent\u002FReturn 기반\n    public int WrittenCount { get; }\n    public ReadOnlySpan\u003Cbyte> WrittenSpan { get; }\n    public void Reset();  \u002F\u002F 버퍼 반환 없이 재사용\n}\n```\n\n### ProtoWriter (IBufferWriter 기반)\n\n생성된 ProtoWriter는 IBufferWriter\u003Cbyte>를 직접 사용한다:\n\n```csharp\nprivate static class ProtoWriter\n{\n    public static void WriteVarint(IBufferWriter\u003Cbyte> w, ulong value);\n    public static void WriteTag(IBufferWriter\u003Cbyte> w, int tag, int wireType);\n    public static void WriteString(IBufferWriter\u003Cbyte> w, int tag, string? value);\n    public static void WriteInt32(IBufferWriter\u003Cbyte> w, int tag, int value);\n    \u002F\u002F ... 등\n}\n```\n\n---\n\n## Frame Buffer Pooling (Hard Rule)\n\n**Proxy.SendXxx 메서드는 PooledBufferWriter + EncodeTo 패턴으로 Zero-Alloc Send를 구현한다.**\n\n### 생성 패턴 (필수)\n\n```csharp\npublic void SendFoo(Foo message)\n{\n    var session = _session ?? throw new InvalidOperationException(...);\n\n    using var bw = new PooledBufferWriter(256);\n\n    \u002F\u002F Write opcode (4 bytes LE)\n    var span = bw.GetSpan(4);\n    BitConverter.TryWriteBytes(span, Opcodes.Foo);\n    bw.Advance(4);\n\n    \u002F\u002F Encode payload directly to buffer\n    _codec.EncodeTo(bw, message);\n\n    \u002F\u002F Send frame\n    session.SendTo(bw.WrittenSpan);\n}\n```\n\n### 핵심 규칙\n\n1. **`new byte[]` 금지**: Send 경로에서 `new byte[...]` 또는 `_codec.Encode()` 사용 금지\n2. **PooledBufferWriter 사용**: opcode + payload를 연속으로 기록\n3. **using 패턴**: `using var bw = new PooledBufferWriter(256);`로 자동 Dispose\n4. **EncodeTo 사용**: `_codec.EncodeTo(bw, message)`로 직접 인코딩\n\n### Legacy API 보존 (호환성)\n\n- `ICodec.Encode\u003CT>()`: 내부적으로 `EncodeTo` 호출 후 `ToArray()` (레거시 호환)\n\n### DoD (완료 정의)\n\n- [ ] ICodec에 `EncodeTo\u003CT>(IBufferWriter\u003Cbyte> writer, T message)` 존재\n- [ ] 생성된 `SendXxx`에서 `using var bw = new PooledBufferWriter(...)` 패턴 사용\n- [ ] 생성된 `SendXxx`에서 `_codec.EncodeTo(bw, message)` 호출\n- [ ] `_codec.Encode(message)` 또는 `new byte[...]`가 SendXxx에 없음\n- [ ] `CodecProtobuf.Encode\u003CT>()`는 내부적으로 `EncodeTo` 사용\n\n---\n\n## Zero-Alloc Decoding (Hard Rule)\n\n**프로토콜 디코딩은 임시 byte[] 할당 없이 span 기반으로 구현한다.**\n\n### ProtoReader (Span 기반)\n\n```csharp\ninternal ref struct ProtoReader\n{\n    private static readonly Encoding Utf8 = Encoding.UTF8;\n    private ReadOnlySpan\u003Cbyte> _data;\n    private int _pos;\n\n    public int Position => _pos;\n    public int Remaining => _data.Length - _pos;\n}\n```\n\n### String Decode: 임시 byte[] 금지 (Hard Rule)\n\n**Reader는 연속 span이 가능하면 `Encoding.UTF8.GetString(ReadOnlySpan\u003Cbyte>)`로 디코드한다.**\n\n```csharp\npublic string ReadString()\n{\n    var len = (int)ReadVarint();\n    if (len == 0) return string.Empty;\n    \u002F\u002F Zero-alloc path: decode directly from contiguous span\n    var span = _data.Slice(_pos, len);\n    _pos += len;\n    return Utf8.GetString(span);  \u002F\u002F string 1회만 할당\n}\n```\n\n**핵심 규칙:**\n1. 연속 span이면 `Utf8.GetString(span)` 직접 호출 (string 할당만 발생)\n2. 연속 span이 불가능한 경우에만 ArrayPool 임시 버퍼 rent\u002Freturn\n3. `new byte[]` 임시 버퍼 생성 금지\n\n### Collection Decode: 교체 금지 (Hard Rule)\n\n**repeated\u002Fdict 필드는 디코드 시 컬렉션을 새로 만들지 않고 Clear() 후 채운다.**\n\n```csharp\ninternal static void DecodeFoo(ReadOnlySpan\u003Cbyte> data, Foo m)\n{\n    \u002F\u002F Clear collections before decode (reuse pattern - no new List allocation)\n    m.Items?.Clear();\n\n    var reader = new ProtoReader(data);\n    while (reader.HasMore)\n    {\n        var (tag, wireType) = reader.ReadTag();\n        switch (tag)\n        {\n            case 1:\n                \u002F\u002F Only create if null (first decode), never replace\n                (m.Items ??= new List\u003Cint>()).Add(reader.ReadInt32());\n                break;\n        }\n    }\n}\n```\n\n**핵심 규칙:**\n1. **리스트 교체 금지**: `m.Items = new List\u003CT>()` 패턴 금지\n2. **Clear() 패턴 사용**: 디코드 전 `?.Clear()` 호출\n3. **null-coalesce 생성**: `??= new List\u003CT>()` 로 최초 1회만 생성\n4. **AddRange 임시 배열 금지**: 루프에서 개별 Add() 사용\n\n### Packed Repeated: Span 기반 파싱 (Hard Rule)\n\n**packed repeated는 length-delimited 블록을 span으로 직접 순회하며 파싱한다(복사 금지).**\n\n```csharp\n\u002F\u002F ProtoReader에 제공되는 헬퍼\npublic int ReadLengthDelimitedEnd()\n{\n    var len = (int)ReadVarint();\n    return _pos + len;\n}\n\n\u002F\u002F 사용 패턴 (블록 전체 복사 없이 직접 순회)\nvar end = reader.ReadLengthDelimitedEnd();\nwhile (reader.Position \u003C end)\n{\n    list.Add(reader.ReadInt32());\n}\n```\n\n### DoD (완료 정의)\n\n- [ ] string decode 경로에 `new byte[]` 없음\n- [ ] string decode는 `Encoding.UTF8.GetString(ReadOnlySpan\u003Cbyte>)` 사용\n- [ ] fallback 복사가 필요한 경우 `ArrayPool\u003Cbyte>.Shared.Rent\u002FReturn`만 사용\n- [ ] repeated\u002Fdict 디코드에서 `new List\u003C>`, `new Dictionary\u003C>` 교체 금지\n- [ ] 디코드 전 `?.Clear()` 호출로 기존 컬렉션 재사용\n- [ ] packed repeated는 블록 span 직접 순회 (임시 배열 복사 금지)\n- [ ] 컴파일 에러 없음\n\n---\n\n## Reference\n\n- Policy SSOT: `skills\u002Fdevian\u002F10-module\u002F03-ssot\u002FSKILL.md`\n- 동작 정본: 런타임\u002F제너레이터 코드\n",{},"main","8cb5bd877a3a7004867d2f55697cb9e4ff2f1734","skills\u002Fdevian\u002F80-tools\u002F11-builder\u002F33-protocol-spec","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft","Kim Hyong Joon","2026-04-12 18:44:10",{"source":7,"skillName":5,"commands":33,"prompt":43,"zipAvailable":44},[34,37,40],{"tool":35,"command":36},"npx","npx skills add devwinsoft\u002Fdevian --skill 33-protocol-spec",{"tool":38,"command":39},"bunx","bunx skills add devwinsoft\u002Fdevian --skill 33-protocol-spec",{"tool":41,"command":42},"pnpm","pnpm dlx skills add devwinsoft\u002Fdevian --skill 33-protocol-spec","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F11-builder\u002F33-protocol-spec\u002FSKILL.md",true,[46,52],{"path":47,"resourceType":48,"resourceTypeDesc":49,"sizeBytes":50,"sourceHtmlUrl":51},"agents\u002Fopenai.yaml","REFERENCE","参考文档",133,"https:\u002F\u002Fgithub.com\u002Ffernando-delosrios-sp\u002Fsailpoint-utils\u002Fblob\u002Fmain\u002FEntro\u002Fentro-connect\u002F.agents\u002Fskills\u002Fgit-commit\u002Fagents\u002Fopenai.yaml",{"path":53,"resourceType":48,"resourceTypeDesc":49,"sizeBytes":54,"sourceHtmlUrl":55},"references\u002Fprivate-data.md",5204,"https:\u002F\u002Fgithub.com\u002Ffernando-delosrios-sp\u002Fsailpoint-utils\u002Fblob\u002Fmain\u002FEntro\u002Fentro-connect\u002F.agents\u002Fskills\u002Fgit-commit\u002Freferences\u002Fprivate-data.md",[57,63,71,79,88,95],{"skillNo":58,"name":59,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":60,"categories":61,"keywords":62,"publishedAt":21},"01M6CBX3K2JJV38ANHVGCAT8A0","devian","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002FSKILL.md",[],[59],{"skillNo":64,"name":65,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":68,"publishedAt":21},"01M6CBX210BFMM8AMN770J9WY8","90-project-archive","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F90-project-archive\u002FSKILL.md",[],[69,70],"archive","90 project archive",{"skillNo":72,"name":73,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":74,"categories":75,"keywords":76,"publishedAt":21},"01M6CBWZSGRR9R3YJZB3177667","30-env-management","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F30-env-management\u002FSKILL.md",[],[77,78],"management","30 env management",{"skillNo":80,"name":81,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":21},"01M6CBWYD3TGWBQV7V35551THF","19-page-push-send","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F19-page-push-send\u002FSKILL.md",[],[85,86,87],"push","page","19 page push send",{"skillNo":89,"name":90,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":91,"categories":92,"keywords":93,"publishedAt":21},"01M6CBWWRQ05A7604GHJKTPJTF","18-page-savedata","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F18-page-savedata\u002FSKILL.md",[],[86,94],"18 page savedata",{"skillNo":96,"name":97,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":7,"ownerLogin":8,"ownerAvatarUrl":9,"ownerAccountNo":10,"ownerDisplayName":11,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":100,"publishedAt":21},"01M6CBWVH4S2VF1V7B2GBTESCT","17-page-deobfuscate","https:\u002F\u002Fgithub.com\u002Fdevwinsoft\u002Fdevian\u002Fblob\u002Fmain\u002Fskills\u002Fdevian\u002F80-tools\u002F20-operation\u002F17-page-deobfuscate\u002FSKILL.md",[],[86,101],"17 page deobfuscate"]