# M2 자율 구현 규약 — 11라운드 지시서 > **이 파일이 무엇인가**: **[2026-08-28 신설]** M2(반응형 코어) 구현을 자율 구간으로 > 돌리기 위한 규약이자, 단위가 끝날 때마다 띄우는 **11라운드 탐사자**에게 그대로 > 주는 지시서다. 산출물은 `m2-implementation-round11.md`(발견 원문 + > §4 배치 문항지). 9·10라운드 지시서(`-round9-brief.md`/`-round10-brief.md`)와 > 같은 관례이되, **이번 라운드는 종이가 아니라 실제 코드가 감사 도구다** — > 10라운드 지시서가 *"이후로는 M2 구현 자체가 더 나은 감사 도구"*라고 예고한 > 그 자리. > > **왜 이 규약인가**: 사용자 결정(2026-08-28) — 별도 에이전트가 초안한 > 규약을 검토해 순서 오류 하나(`EpochMap`이 `Source/State` 뒤에 가 있던 것)와 > 소스 단일화 몇 건을 고친 뒤 채택. 사용자 원문: *"수정하고 너가 진행하자. > epochmap 순서 하나 고치고 진행할 수 있겠니?"* 원칙은 10라운드와 같다 — > *"인간을 기다리는거 엄청 비효율이라서 … batch 로 처리될 필요가 있는듯"* > (`-round10-brief.md`). 발견을 하나씩 물으러 오지 말 것. > > `conventions.md`의 "작업 방식"에 짧은 항목이 있고 **본문은 이 파일이 소스**다. --- ## §1 범위와 순서 - **단위(unit)** 넷. 단위 하나가 끝날 때마다 §4 "관여 시점"으로 온다. 1. **공통 기반** — `ROADMAP.md`의 "공통 기반 — 반응형보다 먼저" 절 전부: `Brand.luau` / `Relate.luau`(M1에 이미 커밋됨 — 남은 건 `base/relate-plan.md` 대조 + 테스트) / `LifetimeHandle.luau` 인터페이스 / `Ref.luau` 최소형(`H-128`) / `Void`(`H-162`). **여기에 `H-97`의 mock 생명주기 4종**(`bindLifetime`/`unbindLifetime`/`canBound`/`canExecute`의 mock 백엔드 구현)을 끼운다 — `ROADMAP.md`엔 M2 꼬리에 있지만 이게 없으면 두 번째 단위부터 "구현 + 테스트 짝" 원칙이 성립하지 않는다(전파 루프가 매 발화마다 `canExecute`를 부른다). 2. **`EpochMap` → `Source`/`State`/`Store`** — **`EpochMap.luau`가 State 본체보다 먼저다.** `ROADMAP.md` "반응형 본체" 절이 *"`Source.luau`/`State.luau`가 이걸 전제로 짜여야 하므로 `EpochMap.luau`(위 항목)가 State 본체보다 먼저 온다"*로 못 박아 뒀다(`State`가 `valueEpochMap`/`emitEpochMap`을 컴포지션). 초안 규약이 `EpochMap`을 `Effect` 뒤에 뒀던 것이 이 규약이 고친 유일한 순서 오류다. 3. **`Observer` → `Effect`** — `Effect`는 Observer 뒤(의존). 4. **`GateNode` → `Blocker`** + `quad-types`의 `Quad` 탑레벨 값 전부(`H-80`) + **mock 대상 전파 루프 테스트**(`ROADMAP.md` M2 마지막 항목). 단위 안의 순서는 `ROADMAP.md` 체크박스 순서를 따른다. - **각 모듈은 "`base/` 의사코드를 그대로 옮긴 구현 + 그 절의 계약을 검증하는 테스트"로 짝지어** 진행한다. 테스트는 **`./scripts/test.sh`로만** 돌린다 — `luau` CLI가 심볼릭 링크를 못 타서 그냥 돌리면 거짓 클린이 난다 (`project-context.md`). - **코드 배치와 테스트 파일 이름**은 첫 단위 계획(§6)이 정한다 — 이 규약이 아니라 그 계획이 소스. ## §2 세 갈래 — 발견은 이렇게 분류해서 처리한다 1. **자율로 고치고 넘어가는 것**: `base/` 의사코드를 옮기다 드러나는 오류(순서, 빠진 `end`, 이름 불일치, 문서 간 stale), 테스트 실패 중 **"문서가 이미 답을 갖고 있는 것"**. → `H-nnn` 번호를 매겨 `m2-implementation-round11.md`에 기록하고 **`base/`와 코드를 같은 커밋에서** 고친다. 묻지 않는다. 번호는 **`H-165`부터** (10라운드 후속이 `H-164`까지 썼다). 2. **모아서 올리는 것**: 새 필드·인자·이름·표면·메커니즘이 필요해지는 것, 확정을 뒤집어야 하는 것, "이 갈래들이 공유하는 전제"가 흔들리는 것. → **코드에 넣지 말고** `round11.md` §4 표에 **갈래 + 권고 + 권고 근거**로 쌓는다. 권고가 "옛 메커니즘 복원"이면 **그 표시를 단다** — 2026-08-28의 `Refresh` 복원 권고(`H-159`)가 `_rerunRequired`로 뒤집혔듯, 그런 자리는 사용자가 다른 모양을 갖고 있을 가능성이 높다. 막힌 부분은 코드에 **`-- TODO(H-nnn): 한 줄 요지`** 마커 + 문항 번호로 남기고 그 모듈의 나머지를 계속 진행한다. 마커 형식을 이 하나로 고정하는 이유: `quad-doc-auditor`는 코드를 안 보므로 `grep -rn "TODO(H-" quad-base/src`가 잔여를 전수 확인하는 유일한 수단이다. 3. **즉시 멈추고 사용자를 부르는 것**: 2번 중에서 **이미 짠 코드의 상당 부분을 무효화할 규모**의 전제 흔들림 — 그 위에 코드를 더 쌓으면 손해인 경우만. 멈추고 상황을 한 문단으로 보고한다(자율 구간이라 사용자가 즉답할 수 없으니, 보고 자체가 그 세션의 끝이다). ## §3 리뷰·감사 발견의 취급 - `/code-review`와 `quad-doc-auditor`가 **"새 메커니즘"으로 분류한 발견은 반영하지 말고 2번으로 쌓는다.** 이 코퍼스의 반복 실패 모드(`token`, 전용 에러 문구, `wasAlive`)가 정확히 "리뷰 제안을 승인된 것처럼 넣은 것"이었다 (`conventions.md`의 *"새 필드·인자·이름·메커니즘은 발견이지 결정이 아니다"* 항목이 소스). - 반영 전에 (a) 그 책임의 현재 소유자와 (b) 그 모양이 과거에 기각된 적 있는지를 먼저 grep한다 — `base/`의 "검토 후 안 만들기로 한 것"류 목록과 `archive/`. ## §4 관여 시점 - **커밋 게이트는 두 층이다.** - **매 커밋**: `python3 .claude/tools/doc-check.py` ERROR 0. 1번 갈래는 커밋 단위가 작아 단위 하나에 커밋이 여럿 생기는데, 그 중간 커밋도 이 게이트를 탄다. - **단위 끝**: 감사 루프(`quad-doc-auditor` **한 턴에 하나**, diff 범위, **`git stash` 금지**를 프롬프트에 직접 명시, 새 발견 0건까지) → doc-check ERROR 0 → `/code-review high` → 커밋. 그 뒤 **신선한 컨텍스트의 fable 탐사자 하나**를 이 파일(§5)을 지시서로 띄워 실제 코드를 돌려보고 `round11.md`에 발견을 이어 붙이게 한 다음, 사용자에게 **"`round11.md` §4를 보라"고 한 줄로** 알린다. 사용자는 §4 표만 읽고 갈래를 배치로 회신한다. - 세션이 일단락될 때마다 `session/YYYY-MM-DD-NN-slug.md` 원문을 남기고, `todos.md` 00번·`project-context.md`·`CLAUDE.md` 머리말을 **"M2 진행 중"**으로 갱신한다(`conventions.md`의 세션 원문 규율 그대로). - 비용 참고: 단위당 감사 루프 + `/code-review high` + 탐사자 조합은 수십만 토큰이다. 그래서 단위를 넷으로 쪼갰다 — 관여 시점이 촘촘해지고 3번 갈래의 손실도 작아진다. ## §5 탐사자 지시 (단위 끝마다 띄우는 fable 탐사자에게) 당신은 Roblox 엔진용 DOMless UI 렌더러 **quad**의 M2 구현을 감사한다. 저장소 루트가 작업 디렉토리다. 당신은 신선한 컨텍스트에서 시작한다 — 앞선 세션의 가정을 물려받지 않는 것이 당신의 가치다. **`git stash`를 쓰지 말 것**(작업 트리 대조는 `git show HEAD:<경로>` / `git diff HEAD -- <경로>`). 1. 먼저 읽을 것: `CLAUDE.md` → `.claude/conventions.md`(특히 *"새 필드·인자·이름· 메커니즘은 발견이지 결정이 아니다"*와 *"하나의 무언가가 두 일을 하고 있지 않은지"*) → 이 파일 §1~§3 → `m2-implementation-round11.md`(지금까지의 발견, 당신의 번호는 마지막 번호 다음부터). 2. 대상: 이번 단위의 커밋 범위(`git log`로 확인)에 들어온 `quad-base/src/`· `quad-base/test/`와, 그 코드가 옮겨 적은 `base/` 절. 3. 할 일: (a) **코드를 실제로 돌린다** — `./scripts/test.sh` 전체, 그리고 계약 절이 요구하는데 테스트가 없는 경로는 임시 스크립트로 직접 태운다(임시 파일은 `quad-base/test/`에 남기지 말고 발견 본문에 인라인). (b) 코드와 `base/` 절을 **한 줄씩 대조**한다 — 옮기다 바뀐 것, 문서에 없는 분기, 문서엔 있는데 코드에 없는 것. (c) `grep -rn "TODO(H-" quad-base/src`로 남은 마커를 전수 확인한다. 4. 산출물: `round11.md`에 `H-nnn`으로 이어 붙인다. **§2의 세 갈래로 분류해서 적되, 1번 갈래도 직접 고치지 말고 발견으로만 남긴다**(수정은 메인 세션이 한다). 2번 갈래는 §4 표에 갈래 + 권고 + 권고 근거, "옛 메커니즘 복원" 표시. 5. 이상 없다고 확인한 자리도 §5 형식으로 적는다(다음 탐사자가 다시 파지 않게). ## §6 첫 단위(공통 기반) 작업 계획 — **[2026-08-28 사용자 확정]** (*"진행하면 될것 같아"*) 소스는 `ROADMAP.md` "공통 기반 — 반응형보다 먼저" 절 + `H-97`. 여기 적힌 배치 결정 셋(브랜드 인스턴스 위치 / mock 생명주기 위치 / 테스트 파일 이름)은 `base/`가 정하지 않은 **코드 배치**라 이 계획이 소스다 — 설계 결정이 아니다. **소스 (`quad-base/src/`)** | 파일 | 내용 | 옮겨 적는 절 | |---|---|---| | `Brand.luau` | `Brand()` 생성자 + **브랜드 인스턴스 전부를 이 잎 파일에**(개수는 그 파일이 소스)(`brand-plan.md` 스니펫이 한 자리에 선언하는 그대로 — `EpochBrand`를 `Source`/`Ref`/`GateNode`가 공유하므로 타입 모듈마다 두면 순환 require) + M2 타입의 `is*`(`isEpoch`/`isSource`/`isState`/`isStore`/`isObserver`/`isEffect`/`isBlocker`/`isRef`/`isPreRef`/`isPostRef`/`isModifier`). `isTag`/`isAttribute*`/`isTween`/`isSlot`은 그 타입의 마일스톤에서 | `base/brand-plan.md` "구현 — 인스턴스 브랜드" / `isRef` 계층 절 | | `Relate.luau` | **기존 파일**(M1). 코드 변경 없음 — `base/relate-plan.md` "API"/"실제 구조" 대조 + 테스트만 | `base/relate-plan.md` | | `LifetimeHandle.luau` | 4종 타입 시그니처 + **미주입 에러 스텁**(영어, `error(…, 2)`) + 공유 술어 `isBoundAlive`는 백엔드 몫이라 여기 없음 | `base/lifecycle-pattern.md` "확정" 절, `module-lifecycle-plan.md` 주입 절 | | `Ref.luau` | 최소형 — `.Value`/`.Revision`/`:Set`/`:Callback`/`:WeakCallback`/`:Uncallback`, `Callbacks`(강) + `WeakCallbacks`(weak-key), `:Set` 순서 값→리비전(`bit32.bnot(-rev)`)→스냅샷 순회·함수키 dedup·thread 소진, `EpochBrand`+`RefBrand` 등록 | `base/ref-plan.md` "`Ref`는 `Epoch`를 만족한다" 절 + `H-128` | | `Void.luau` | `return function() end` | `H-162`, `architecture.md` 잎 모듈 | | `init.luau` | `Relate`/`Void`/`Ref`/`is*`/생명주기 4종 스텁 재export | `H-80` | | `quad-types/src/init.luau` | `Quad` 타입에 위 탑레벨 값 추가(`Source`/`Store`/`Effect`/`Blocker`는 각 단위에서) | `H-80`/`H-25` | **mock (`quad-base/test/mock.luau`)** — `H-97` 4종을 **이 파일 안에** `installLifetime(quad)`로 둔다(quad-roblox가 할 모듈 뮤테이션을 그대로 흉내; `Destroying:Connect`의 `Connection`을 `Relate` weak 슬롯 `"gcconn"`에 두고 `.Connected`로 판정, `.Subscribed` 경로는 단위 3에서 합류). 별도 파일을 안 만드는 이유: mock 백엔드가 곧 이 파일 하나다. **테스트 (`quad-base/test/spec..luau`)** — `scripts/test.sh`의 glob을 `smoke.*` + `spec.*`로 넓히고, relink 뒤 `luau-analyze quad-base/src`도 같이 돌린다 (relink가 거짓 클린의 원인을 없애므로 analyze가 이제 의미 있다). | 파일 | 검증하는 계약 | |---|---| | `spec.brand.luau` | register/is · 다중 태깅 · 브랜드 간 독립 · weak-key(GC 뒤 사라짐) · `isRef(PreRef 등록값) == true`, `isPreRef`/`isPostRef` 배타 | | `spec.relate.luau` | 4 메서드 · 서브테이블 lazy 생성 · `WeakMap` 값 GC · `inst` weak-key GC · 공유 메타테이블 | | `spec.lifetime.luau` | 미주입 스텁이 영어 메시지로 error · 주입 후 `bindLifetime` → `canBound` false/`canExecute` true · `Destroy` → 반대 · `unbindLifetime` 조기 해제 · 이중 바인드 게이트 모양 `if not canBound then error(…, 2)` | | `spec.ref.luau` | 초기 `Revision` · `:Set` 순서(콜백이 볼 때 이미 새 값·새 리비전) · `bit32` 랩(0 → 4294967295) · `Callback`/`WeakCallback`/`Uncallback` · `fn(value, ref)` · 같은 fn 양쪽 등록 시 1회 · weak 콜백 GC 뒤 침묵 · 발화 중 `Uncallback` 안전(스냅샷) · thread 콜백 1회 소진 · `isRef`/`isEpoch` | | `spec.void.luau` | 반환값 없음 · 항등(항상 같은 함수) | | `smoke.init.luau`(갱신) | 탑레벨 값 존재·타입 | **커밋 단위**: 모듈마다 하나(구현 + spec + `base/` 정정이 있으면 같은 커밋), 마지막에 `test.sh`/`quad-types` 커밋. 단위 끝 절차는 §4.