diff --git a/.claude/README.md b/.claude/README.md index 5172c69..54641e8 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -24,7 +24,7 @@ | `base/` | 결정 완료 + 프로젝트 전체에 걸치는 컨텍스트 — plan/done 개념 없음, 계속 참조되는 배경지식. **항상 읽어야 하는** 배경지식만 여기 둠(다른 문서를 이해하는 데 전제되는 것) | | `reference/` | **[2026-08-07 신설]** 결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 참고 자료(v1 스냅샷, 프레임워크 비교 리서치) — "완료" 개념 없는 건 `base/`와 같지만, 항상 읽을 필요는 없고 해당 문서가 인용될 때만 열어보면 됨. `quadnomicon` 소재 후보가 많음 | | `research/` | 아직 착수 전, 사용자와 스코프/설계를 더 상의해야 함 | -| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 3번과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M2가 M3의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영, 마일스톤 재편 여부는 열려 있음). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 research 문서 둘(`gate-plan.md`·`state-epoch-plan.md`)까지. **처리 결과의 소스는 이 파일**). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | +| `qa-request/` | 원래 용도는 "구현 완료(코드/에이전트 검증까지 끝남) + 사용자 본인의 실기기(Roblox Studio) QA만 남음". **[2026-08-18 확장]** 구현 전에도 **사용자 심사 라운드의 산출물**을 여기 둠 — `pre-implementation-qa-round1.md`(1라운드: `base/` 확정 문서 전체를 문항으로 재확인받아 **"아니오"가 나온 항목만** 모은 결함 목록 + 신규 요구사항(`N-n`) + 부수 오탈자. **같은 날 전부 `base/`에 반영 완료**라 지금은 "무엇이 왜 틀렸었나"의 근거 기록이고, 지금 유효한 설계는 항상 `base/`가 소스. 아직 안 닫힌 것은 `question.md` 3번과 `.claude/todos.md` 00번이 소스), `pre-implementation-qa-round2.md`(2라운드: 확정 의사코드를 실제로 손으로 실행해보는 트레이싱 — **완료**, 발견된 크래시 `RC-1`(`recompute` 트리거 모델)도 같은 날 후속 세션에서 Blocker 게이팅 설계로 해결·반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리), `pre-implementation-qa-round3.md`(3라운드: `RC-1` 해법(Blocker 게이팅)이 실제로 `attachSlot`/`recompute`에 반영된 걸 손으로 트레이싱 — **완료**, `RC-3`/`RC-4`(`activateList`가 자기 Slot의 Blocker보다 먼저 실행되는 순서 문제)와 `bk.N` 수명주기 미정을 발견했다가 같은 세션에 사용자가 최초 분석 오류를 직접 정정하며 전부 해결·`base/` 반영까지 끝남, `archive/question-resolved.md`에 논의 요지 정리. `ROADMAP.md` M2가 M3의 `Blocker.luau`에 의존하게 된 마일스톤 순서 불일치는 각주로 반영, 마일스톤 재편 여부는 열려 있음). `pre-implementation-qa-round4.md`(4라운드: 사용자 요청으로 **`base/` 확정 전체를 "예가 나와야 정상인 문항"으로 다시 뽑은 전수 문항지** — **[2026-08-21] 완료·종결**. 1~3라운드와 달리 이 파일은 **문항지 원본 그대로 남긴다** — 처리 결과 전량이 `-followup.md`에 쌓였으므로 "아니오만 남기는 재편"은 안 하기로 함(같은 정보가 두 곳에 갈라지는 걸 피함). 문항 수/문서별 분포는 그 파일 자신이 소스). `pre-implementation-qa-round4-response.md`(사용자 회신 원문 — 이 라운드는 회신을 **별도 파일**로 받았다, 1~3라운드와 다른 점), `pre-implementation-qa-round4-followup.md`(**[2026-08-20 시작, 2026-08-21 종결]** 그 회신 처리 결과 — A~H 8개 절이 4차에 걸쳐 시간순으로 쌓였고 **마지막 H절이 최신이자 소스**. B·C절의 재질문/판단 대기 항목은 F·G절을 거쳐 **H절에서 전량 닫혔다**(`Detach` 보존 주체, `KeyGone`, `Owned`, `attachSlot` 분해). **열린 질문 없음**. **[2026-08-21 정정]** 여기 적혀 있던 "5라운드 문항지는 만들지 않는다"는 뒤집혔다 — 같은 날 사용자 요청으로 5라운드를 만들었다), `pre-implementation-qa-round5.md`(**[2026-08-21 신설·처리 완료]** 5라운드: 4라운드에서 "예"로 넘어간 자리는 건너뛰고 **(1) 4라운드에 문항이 아예 없던 영역**(`project-setup-plan.md`/`quad-types-plan.md`, 그리고 **문서가 아니라 실제 커밋된 M1 코드**), **(2) 4라운드 회신 이후 새로 확정된 것**(`Detach`/`_detached`/`KeyGone`/`Owned`/`attachSlot` 분해 등), **(3) 큰 문서의 심화**(예: `debounce-throttle-plan.md`)만 묻는다. 문항 수는 그 문서 자신이 소스), `pre-implementation-qa-round5-response.md`(사용자 회신 원문 — 4라운드와 같이 별도 파일), `pre-implementation-qa-round5-followup.md`(**[2026-08-21]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 문서 둘(`gate-plan.md`·`state-epoch-plan.md` — 같은 날 확정되며 `base/`로 승격)까지. **처리 결과의 소스는 이 파일**). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | | `archive/` | 완료 + 사용자가 실사용/실기기로 직접 검증까지 마침 (구현 대상). **[2026-08-06 확장]** 완전히 뒤집힌 설계 결정을 원문+역전 이유+diff와 함께 보존하는 용도로도 사용(제목 `[역전됨]` — 한 번 확정했다가 뒤집힌 것) — 더 이상 능동적으로 참고 안 해도 되지만(토큰 낭비 방지 위해 `base/`/`research/`에서 뺌) `quadnomicon` 소재로는 나중에 쓸 수 있음. **[2026-08-07 확장]** 후보였다가 채택 안 된 것(확정한 적 없이 검토 후 기각)도 같은 방식으로 보존, 제목은 구분을 위해 `[기각됨]` — `[역전됨]`과 의미가 다르므로 혼동하지 말 것. **[2026-08-07 세 번째 확장]** 설계 반전/기각과 별개로, 에이전트가 문서 작성 중 스스로 낸 개념 혼동을 정정한 이력은 `[에이전트 실수]` 태그로 `agent-mistake.md` 하나에 모음(`.claude/session-summary.md`/`session/` 로그와의 중복 방지) | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — **[2026-08-19 기준] 폴더 자체가 아직 없음**(M0/M1 스캐폴딩만으론 안 생기고 실제로 렌더링해보고 쓰는 단계부터, 첫 피드백이 생길 때 만들면 됨). `qa-request/`는 **[2026-08-18] 더 이상 비어 있지 않음**(구현 전 QA 1라운드 산출물이 들어감) — 여긴 아직 폴더도 없음 | | `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`가 **비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` | diff --git a/.claude/base/gate-plan.md b/.claude/base/gate-plan.md index 231e7d2..a444df0 100644 --- a/.claude/base/gate-plan.md +++ b/.claude/base/gate-plan.md @@ -5,8 +5,9 @@ 없이 `state:Gate( (emit) -> ()->() )` 처럼 선언되고 마치 Compute 처럼 GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 배선을 따른다 ← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**("게이팅 먼저" -결정, `ROADMAP.md`). **남은 것은 생명주기·재진입 계약과 M2 범위뿐**(아래 -"아직 안 정한 것"). +결정, `ROADMAP.md`). **남은 것은 아래 "아직 안 정한 것"** — 그중 **4번(유보된 +emit이 싣는 source)은 `setup` 시그니처를 바꿀 수 있어 M2 착수 전 판단이 +필요하다**(2026-08-21 `/code-review high` 발견). **⚠️ 처음 방향이 한 번 바뀌었다.** 신설 당시엔 *"공용 `Gate` 프리미티브를 꺼내고 `Blocker`가 그걸 컴포지션한다"*였는데, 확정된 형태는 **프리미티브를 따로 안 @@ -15,8 +16,9 @@ GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 표면을 주기도 하구요."* 아래 본문 중 "공개 프리미티브로 꺼낸다"류 서술은 그 이전 시점 표현이니 이 배너 기준으로 읽을 것. -**한 줄**: `Blocker`가 쓰는 "게이티드 State 노드"를 한 겹 일반화해서, **상류 -emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 공개 프리미티브로 꺼낸다. +**한 줄**: `Blocker`가 쓰던 "게이티드 State 노드"를 한 겹 일반화해서, **상류 +emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 **`state:Gate(setup)` +공개 메소드**로 낸다(탑레벨 생성자가 아니라 — 위 배너 참고). `Blocker`/`Debounce`/`Throttle`이 그 위에 얹히는 서로 다른 정책이 된다. ## 왜 지금인가 — 두 갈래가 같은 자리를 가리켰다 @@ -37,7 +39,8 @@ emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 공 하고, 바깥에서 관측만 해서는 순서를 보장할 수 없다. **공개 여부**: 사용자 판단 — *"이 API가 비공개일 이유는 없어보인다."* 즉 -내부 배관이 아니라 공개 프리미티브로 낸다. +내부 배관이 아니라 **공개 표면**으로 낸다. **[2026-08-21]** 다만 그 표면은 +탑레벨 프리미티브가 아니라 **State 메소드 `:Gate`** 다(아래 2번). ## 제안된 모양 (사용자 스케치 그대로) @@ -113,20 +116,47 @@ end) `base/state-epoch-plan.md`가 이 계약에 **의존**한다 — 그 문서 §5의 3번이 "게이트를 에포크 경계로 만드는" 대안을 기각한 이유가 정확히 이 계약을 뒤집지 않기 위해서다. -4. **생명주기.** 게이트 노드가 잡는 자원(타이머/플래그)이 언제 죽는가 — +4. **⭐⭐ [2026-08-21 신설 — M2 표면에 영향] 게이트가 유보했다 내보내는 emit은 + 어느 source를 싣는가.** 확정된 `setup`은 `(emit: () -> ()) -> (() -> ())`로 + **양쪽 다 source를 안 받는다.** 그런데 `base/state-epoch-plan.md` §2의 수신 + 규칙 셋은 전부 `[source]` 키로 판정한다 — 게이트가 `A:Set(); Z:Set()`을 + 묶어뒀다가 `blocker:Off()`로 한 번에 내보내면, 그 emit이 **어떤 source도 + 지목하지 못한 채** 하류에 도착한다. 그러면 하류는 3번 규칙으로 **삼켜버리고 + 배치 통지가 통째로 사라진다.** 5라운드 M절이 이미 *"`nil` 규약만 `Gate` + 설계와 같이 확정하면 된다"*고 짚어뒀는데 표면 확정 때 같이 안 닫혔다 + (2026-08-21 `/code-review high` 발견). + 후보 셋 — 어느 쪽이든 `setup`/`emit` 시그니처가 바뀐다: + - **(a) `emit(nil)` = 전체 확인.** 받는 쪽이 `sourceCountMap` 전체를 훑는다. + M절의 원안. 비용은 2~4칸 순회라 작지만, "판정은 O(1)"이라는 §3 서술에 + 예외가 하나 생긴다. + - **(b) 게이트가 유보한 source 집합을 기억했다가 해제 때 각각 emit.** + 판정은 O(1) 그대로. 대신 유보된 소스가 N개면 하류가 **N번** 통지받아 + `Blocker`의 "정확히 1회"가 깨진다 — `Blocker` 목적과 정면 충돌이라 + 그대로는 못 쓴다. + - **(c) `GateNode` 자신을 source처럼 취급.** 해제 emit이 `emit(self)`를 + 싣고 게이트가 자기 count를 하나 올린다. 하류는 정확히 1회 통지받고 + 판정도 O(1). 대신 하류의 맵에 루트 Source가 아닌 노드가 섞이므로 §2의 + "루트 Source들의 에포크"라는 서술을 넓혀야 한다. + **에이전트 권고는 (c)** — `Blocker` 계약("정확히 1회")과 O(1) 판정을 둘 다 + 지키는 유일한 안이고, "게이트가 하류에게는 새 원천처럼 보인다"는 게 게이트의 + 실제 의미와도 맞는다. 단 `Get()` 계약(통지만 막음)은 그대로 유지된다 — + §5-3이 기각한 "게이트를 **에포크 경계**로 만들기"와는 다르다. 그건 하류가 + 루트 Source를 **못 보게** 만드는 안이고, (c)는 루트 Source를 그대로 보면서 + 게이트를 **하나 더** 얹는 것뿐이다. +5. **생명주기.** 게이트 노드가 잡는 자원(타이머/플래그)이 언제 죽는가 — 지금 설계대로면 다운스트림이 다 죽으면 GC(팩토리는 weak 추적, `debounce-throttle-plan.md` 5-4). `Gate` 자체에 `Flush`/`Cancel` 같은 표면을 둘지, 그건 정책(Debounce)만의 것으로 둘지. -5. **재진입.** `Blocker`의 "재진입 의도적 미지원"(`blocker-plan.md`)이 `Gate` +6. **재진입.** `Blocker`의 "재진입 의도적 미지원"(`blocker-plan.md`)이 `Gate` 레벨의 계약으로 올라가는지 — 즉 `onUpstreamEmit` 안에서 같은 게이트의 `emit()`을 재귀적으로 부르는 경우. -6. **⭐ 소비자가 하나 더 있다 — `Effect(fn, ...deps)`의 최초 1회 억제.** +7. **⭐ 소비자가 하나 더 있다 — `Effect(fn, ...deps)`의 최초 1회 억제.** 2026-08-21 5라운드 `C-6`에서 확정된 다중 의존성 `Effect`는, 의존성마다 구독을 걸면 각 구독의 "등록 즉시 1회 실행"이 N번 발화하므로 **설치 구간 동안 발화를 눌러뒀다가 마지막에 한 번만 실행**해야 한다(`base/effect-plan.md`의 그 절). 즉 `Gate`(또는 `Blocker`의 직접 사용)가 **"설치 구간을 감싸 최초 발화를 한 번으로 접는" 용례까지 커버해야** 한다 — 설계할 때 이 소비자를 같이 볼 것. -7. **M2 범위.** M2에 `Gate`만 넣고 `Blocker`는 M3에 그대로 둘지, 아니면 +8. **M2 범위.** M2에 `Gate`만 넣고 `Blocker`는 M3에 그대로 둘지, 아니면 `Blocker`까지 같이 앞당길지. `Dispatch.drive`의 배치 등록이 실제로 쓰는 건 `blocker:On()`/`OffWithoutEmit()`/`IsOn()`이므로(배치 게이팅 절), **최소한 그 세 메서드가 도는 형태까지는 M2에 필요**하다. diff --git a/.claude/base/state-epoch-plan.md b/.claude/base/state-epoch-plan.md index 350067e..92c6654 100644 --- a/.claude/base/state-epoch-plan.md +++ b/.claude/base/state-epoch-plan.md @@ -94,6 +94,10 @@ State 것**이다 — 옛 근거("전파 시점에 갱신된 count가 캐시를 신선한 것으로 오인시킨다")는 여전히 틀렸고, 지금 근거는 **순회가 값과 통지를 비대칭으로 앞당긴다**는 것이다. +**⚠️ [2026-08-21 `/code-review high`] 아래 희소 구현 메모는 "둘 다 상류에서 +복사"와 그대로는 안 맞는다** — 아래 §5의 7번이 소스. 두 맵을 명시적으로 다 +들고 시작하는 게 안전하고, 희소화는 그 규칙이 정해진 뒤 얹을 것. + **구현 메모 — `sourceEmitMap`은 희소 테이블로 두면 된다.** emit이 count를 안 싣고 받는 쪽이 라이브로 읽으므로, 이 테이블에 실제로 필요한 정보는 **"순회가 앞질러 흡수해서 아직 안 던진 소스가 무엇인가"** 뿐이다. 평상시엔 비어 있고 @@ -110,8 +114,10 @@ State 아직 안 왔어도 스스로 재계산**한다. 그래서 `D`는 항상 `(B_new, C_new)`를 얻는다. **`Get()`이 "지금 이 순간의 일관된 값"을 반환한다는 보장이 처음으로 성립**한다. -- **고쳐진다 — 중복 재계산.** 뒤늦게 `C` 쪽 신호가 도착해 `rawInvalid`가 켜져도, - count 비교가 "이미 최신"이라 재계산이 안 일어난다. +- **고쳐진다 — 중복 재계산.** 뒤늦게 `C` 쪽 신호가 도착해도 `sourceCountMap`이 + "이미 최신"이라 `rawInvalid`가 켜지지 않고, 따라서 재계산도 안 일어난다 + (§2의 2번/3번 규칙 — **[2026-08-21 정정]** 여기 "`rawInvalid`가 켜져도"라고 + 적혀 있었으나 §2 규칙상 켜지지 않는다). - **⭐ [2026-08-21 확정] 중복 *통지*도 같이 접는다 — `source-state-plan.md`가 "접지 않는다"고 확정해뒀던 것의 역전이다**(역전 원문은 `archive/always-propagate-no-dedup-superseded.md`). 처음엔 "값만 @@ -146,7 +152,7 @@ State 사용자 추산(*"해시 for은 이미 빠르고, Source가 … 수 자체가 적다. 2~4개에 대해 인덱싱 하는 정도"*)에 동의한다. 덧붙일 것 둘: -- `sourceList`의 크기는 **그 노드 상류에 있는 서로 다른 루트 Source의 수**다. +- 두 맵의 크기는 **그 노드 상류에 있는 서로 다른 루트 Source의 수**다. 체인이 길어져도 안 늘고, `:With`로 합류할 때만 는다. UI 파생값에서 이 수가 큰 경우는 드물다. - **[2026-08-21 정정]** 순회 조건이 뒤집혔으므로 비용 구도도 뒤집힌다 — @@ -164,7 +170,7 @@ State 이전과 다른게 없다고 생각한다."* — 선언 안 한 Source를 클로저로 읽는 건 **지금 모델에서도 똑같이 stale**이고 이 변경이 악화시키는 게 없으므로, 새 조항 없이 기존 "의존성은 선언한다"는 관례 그대로 둔다. -2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceList`가 +2. **동적 의존성.** 조건에 따라 다른 상류를 읽는 계산이면 `sourceCountMap`이 보수적 상위집합이 된다 — 틀리진 않고 재계산이 조금 더 잦아질 뿐이다. 그대로 감수할지 확인. 3. **[2026-08-21 해소] 순회가 발견한 변경을 어떻게 처분하는가 — 두 테이블로 @@ -217,6 +223,29 @@ State `base/source-state-plan.md`의 전파 모델 절은 **채택과 함께 그렇게 다시 썼다.** +7. **⭐ [2026-08-21 신설, `/code-review high`] 두 맵의 초기값·병합·재계산 시 + 갱신 범위 — M3 착수 전 필요.** §2가 "둘 다 상류에서 복사되고 `:With`에서 + 합쳐진다"고만 적고 세 자리를 안 정했다: + - **(a) 재계산이 끝났을 때 `sourceCountMap`을 어디까지 갱신하나.** §2는 + `rawInvalid = false`만 말한다. 그런데 규칙 1은 **발행 소스 항목만** + 건드리므로, `D`가 `A`,`Z`에 의존하고 `A:Set(); Z:Set()`이 연달아 오면 — + `A`의 emit으로 재계산된 `D`의 값은 **이미 `Z`의 새 값을 포함**하는데 + `sourceCountMap[Z]`는 옛 값이라, `Z`의 emit이 규칙 1에 걸려 **같은 값을 + 또 계산**한다. **에이전트 권고: 재계산은 자기가 실제로 읽은 상류 전부에 + 대해 `sourceCountMap`을 갱신한다**(맵의 뜻이 "내 값이 이 소스에 대해 + 최신인가"이므로 이게 직독이다). 그러면 `Z`의 emit은 규칙 2로 떨어져 + **통지는 나가되 재계산은 안 한다** — 통지가 나가는 건 옳다, 하류는 아직 + `Z`의 에포크를 못 봤으므로. + - **(b) 새 노드가 생길 때 두 맵의 초기값.** "상류에서 복사"를 문자 그대로 + 하면, 순회로 `sourceCountMap`만 앞당겨진 상류에서 파생된 새 노드가 **그 + 지연분까지 상속**해야 규칙 2가 성립한다. **에이전트 권고: 복사가 아니라 + "첫 재계산 때 자기가 읽은 상류들의 맵을 합쳐 구성"** — 그러면 새 노드는 + `sourceEmitMap == sourceCountMap`인 깨끗한 상태로 시작하고 (a)와도 맞물린다. + 다만 이건 사용자 원안의 "상류에서 복사" 표현을 바꾸는 것이라 **확인 필요.** + - **(c) `:With` 병합에서 두 상류가 같은 소스에 다른 count를 들고 있을 때.** + (b)를 택하면 자동으로 "그때의 라이브 count"로 통일돼 문제가 사라진다. + (b)를 안 택하면 명시 규칙이 필요하다(더 큰 쪽? 더 작은 쪽?). + ## 6. 곁가지 — 폴링용 sugar 사용자 제안: *"폴링을 위해서는 Apply(Realtime()) 같은 슈거를 주면 된다. diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md index 79e1f6d..20c7a5e 100644 --- a/.claude/luau-test/STATUS.md +++ b/.claude/luau-test/STATUS.md @@ -40,9 +40,9 @@ | 폴더 | 뜻 | 개수 | 누가 처리 | |---|---|---|---| | `review-required/` | **설계가 걸림 — 사람 결정 필요** | **0** | ⭐ 사용자 | -| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 4 | 에이전트 | +| `rewrite-required/` | 스파이크가 낡음(코드가 깨졌거나, 설계가 바뀌어 옛 모델을 검증 중) | 6 | 에이전트 | | `not-run/` | 이 환경에서 못 돌림(Studio 전용) | 0(+헬퍼 1) | 사용자 or MCP 연결 후 에이전트 | -| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 19 | — | +| `done/` | 통과 or 판정 끝, 더 할 일 없음 | 17 | — | **폴더를 옮기는 게 곧 상태 갱신** — 스파이크를 고치거나 돌렸으면 파일을 해당 폴더로 `git mv`하고 아래 표의 줄도 같이 옮길 것. 파일별 "무엇을 왜 @@ -104,13 +104,14 @@ |---|---| | `gc-trigger-helper.server.luau` | 스파이크가 아니라 **헬퍼** — Studio에 `collectgarbage()`가 없어서 GC를 강제 트리거하는 기법. `10`을 돌릴 때 같이 씀 | -## ✅ `done/` — 통과 or 판정 끝 (18건) +## ✅ `done/` — 통과 or 판정 끝 (17건) -**런타임 13개 전원 통과**(crash 0 / FAIL 0 — **[2026-08-21]** `01`이 -`rewrite-required/`로 나가며 14 → 13) — **[열네 번째 세션] `04`/`19`는 +**런타임 12개 전원 통과**(crash 0 / FAIL 0 — **[2026-08-21]** `01`과 `05`가 +`rewrite-required/`로 나가며 14 → 12) — **[열네 번째 세션] `04`/`19`는 검증 대상 설계가 바뀌어 `rewrite-required/`로 이동했고, [2026-08-19] -`05`는 현행 모델로 재작성해 다시 여기로 돌아왔고, 신규 `22`(구 `13` -런타임 절반, PostRef까지 확장)가 합류**: +`05`는 현행 모델로 재작성해 잠시 돌아왔다가 **[2026-08-21] 소스 에포크 +채택으로 다시 나갔으며**, 신규 `22`(구 `13` 런타임 절반, PostRef까지 +확장)가 합류**: | 파일 | 확인된 것 | |---|---| diff --git a/.claude/qa-request/pre-implementation-qa-round5-followup.md b/.claude/qa-request/pre-implementation-qa-round5-followup.md index 33adcbe..008aab4 100644 --- a/.claude/qa-request/pre-implementation-qa-round5-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round5-followup.md @@ -936,7 +936,7 @@ emit 수신 규칙 셋: (1) count가 다르면 둘 다 갱신 + `rawInvalid` + 내부 발생 emit이 같은 진입점을 쓰는 것) — 해법으로는 부족한데, 막는 게이트는 보통 순회하는 노드 자신이 아니라 **상류**에 있어 자기 `rawEmit`을 태워도 누출이 남고, `nil` emit은 하류마다 전체 순회를 강제해 같은 문제를 연쇄시키기 -때문. 상세는 `research/state-epoch-validation.md` §2·§5-3. +때문. 상세는 `base/state-epoch-plan.md` §2·§5-3(같은 날 `base/`로 승격됨). **메모**: 이 분리는 M절에서 **철회했던 `seen`/`computedAt` 분리가 다른 근거로 되살아난 것**이다. 옛 근거는 여전히 틀렸고, 지금 근거는 **순회가 값과 통지를 @@ -951,7 +951,7 @@ state 의 전파를 손대는 작업이라 with 처럼 다른 노드가 나는 `:Apply`** 라는 층위 구분이다. 부수로 `Debounce`/`Throttle`의 `:Apply` 관용구는 그대로 유효하고(팩토리가 내부에서 `:Gate`를 부름), `Blocker` 배선은 이미 확정된 `state:Block(blocker)` 메소드로 자동 해소되며, `__call`은 안 쓴다. -`research/gate-primitive.md`의 2번이 해소로 갱신됨. +`base/gate-plan.md`(같은 날 `base/`로 승격)의 2번이 해소로 갱신됨. --- @@ -999,3 +999,28 @@ Blocker 는 해당 내부 배선을 따른다 ← 동의합니다 해당 방법 M2 각주·M3 체크박스), `README.md`, `question.md`, `todos.md`, `luau-test/`(스파이크 `05`가 `rewrite-required/`로 되돌아감 — 다이아몬드 Observer가 이제 변경당 **1회**만 울어야 하므로 핵심 assert가 정반대가 됨). + +--- + +# P절 — `/code-review high` 12건 (2026-08-21, O절 커밋 직후) + +사용자가 `c58c97a`(O절) 직후 `/code-review high`를 돌려 **12건**이 나왔고 +**전부 유효**했다. 9건은 그 자리에서 수정, **3건은 실제로 안 정해진 설계 +구멍이라 열린 항목으로 세웠다**(임의로 정하지 않음). + +## P-1. 열린 항목으로 승격한 3건 + +| # | 무엇 | 어디로 | +|---|---|---| +| High-1 | **게이트가 유보했다 내보내는 emit이 어느 source를 싣는가.** 확정 `setup`은 `(emit: () -> ()) -> (() -> ())`라 양쪽 다 source를 안 받는데, 에포크 수신 규칙은 전부 `[source]` 키 판정 → `blocker:Off()`의 배치 emit이 하류에서 **삼켜진다**. M절이 *"`nil` 규약만 Gate 설계와 같이 확정하면 된다"*고 짚었는데 O절이 안 닫았다 | `base/gate-plan.md` 4번 + `question.md` 3번. **M2 착수 전 필요**(시그니처가 바뀜). 권고는 (c) `emit(self)` — `GateNode`를 source처럼 취급 | +| Med-2 | **재계산 후 `sourceCountMap` 갱신 범위.** 규칙 1이 발행 소스 항목만 건드리므로 다중 소스 배치에서 **같은 값을 두 번 계산**한다 | `state-epoch-plan.md` §5 7-(a). 권고: 자기가 읽은 상류 전부 갱신 → 뒤따르는 emit은 규칙 2로 "통지만" | +| Med-3 | **두 맵의 초기값·`:With` 병합.** "상류에서 복사"를 문자 그대로 하면 순회가 앞당긴 지연분 상속 여부가 안 정해져 새 노드가 통지를 삼킬 수 있고, 희소 `pending` 구현 메모와도 안 맞는다 | 같은 곳 7-(b)/(c). 권고: 복사 대신 **첫 재계산 때 구성** — 그러면 (c)도 자동 해소 | + +## P-2. 그 자리에서 고친 9건 + +- `state-epoch-plan.md`: §4/§5-2의 `sourceList` 잔재 제거(코퍼스에 `bk.sourceList`라는 **무관한 동명 식별자**가 이미 있어 오독 위험), §3의 "뒤늦은 신호에 `rawInvalid`가 켜져도"를 §2 규칙(안 켜짐)에 맞게 정정. +- `gate-plan.md`: 배너가 부정하는 본문 두 문장("공개 프리미티브로 꺼낸다")을 같이 수정 — `conventions.md`가 최빈 실패로 지목한 패턴. +- `question.md` 1번: `Gate` 이름 항목이 "다음 세션으로 미뤄졌다"로 열린 채였음 → 해소로 갱신. +- `luau-test/STATUS.md`: `05` 이동이 반영 안 된 개수 3곳(`done/` 19→17·18건→17건, `rewrite-required/` 4→6, 런타임 13→12)과 "`05`가 다시 돌아왔다"는 같은 파일 안의 모순 문장. +- `comparison-fusion-vide.md`: 배너 바로 위 본문("신호는 두 번 도착해도")이 배너와 어긋나던 것. +- 이 파일 N절과 `README.md`가 가리키던 `research/` 옛 경로. diff --git a/.claude/question.md b/.claude/question.md index 97e69dd..e683ff2 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -64,8 +64,10 @@ `-er`를 많이 쓰는데 `Gater`는 영어로 어색하다. 에이전트 권고는 **`Gate` 그대로**(`gate`는 이미 행위자가 아니라 **장치**를 가리키는 명사라 `-er`가 불필요 — `Source`/`Ref`/`Slot`/`Tween`도 같은 계열), 대안 후보는 - `Valve`/`Relay`. **설계 자체가 다음 세션으로 미뤄졌으므로 이름도 그때 같이** - — 3번 절의 `Gate` 항목과 `base/gate-plan.md`가 소스. + `Valve`/`Relay`. **[2026-08-21 해소] `Gate`로 확정**(탑레벨 생성자를 안 + 만들고 `state:Gate(setup)` 메소드로 가면서 `Gater` 문제 자체가 사라짐 — + 메소드 자리에서는 `:With`/`:Compute`와 나란히 자연스럽다). 노드 타입 이름은 + `GateNode`. `base/gate-plan.md`의 1번이 소스. - **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가 다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이 헷갈릴 수 있음. @@ -205,6 +207,25 @@ 빈도가 다르다.** 해법은 있다 — reconcile이 `pos`처럼 **절대 offset도 러닝 누적**으로 들고 다니면 O(n)(그게 `mountSlotTree`가 이미 하는 방식). 그렇게 할지, 아니면 실측 전엔 그냥 둘지 판단 필요. +- **⭐⭐ [신설, 2026-08-21 `/code-review high`] 게이트가 유보했다 내보내는 + emit은 어느 source를 싣는가 — M2 착수 전 필요.** 확정된 `setup`은 + `(emit: () -> ()) -> (() -> ())`라 **양쪽 다 source를 안 받는데**, + `base/state-epoch-plan.md`의 수신 규칙은 전부 `[source]` 키로 판정한다. + 그래서 `blocker:Off()`가 묶어뒀던 배치 emit이 하류에서 **삼켜져 통지가 + 통째로 사라진다.** 후보는 (a) `emit(nil)` = 전체 확인, (b) 유보한 소스마다 + 각각 emit(→ `Blocker`의 "정확히 1회"가 깨짐), (c) `GateNode` 자신을 + source처럼 취급해 `emit(self)`. **에이전트 권고는 (c)** — 1회 통지와 O(1) + 판정을 둘 다 지킨다. 어느 쪽이든 `setup`/`emit` 시그니처가 바뀌므로 M2 + 전에 정해야 한다. 상세는 `base/gate-plan.md`의 4번. +- **⭐ [신설, 2026-08-21 `/code-review high`] State 에포크 — 두 맵의 초기값· + 병합·재계산 시 갱신 범위 — M3 착수 전 필요.** 세 자리가 안 정해져 있다: + (a) 재계산이 끝나면 `sourceCountMap`을 **자기가 읽은 상류 전부**에 대해 + 갱신할 것인가(안 그러면 다중 소스 배치에서 같은 값을 두 번 계산한다), + (b) 새 노드의 두 맵을 "상류에서 복사"할 것인가 아니면 **첫 재계산 때 + 구성**할 것인가(원안은 복사인데, 순회가 앞당겨둔 지연분까지 상속해야 규칙이 + 성립한다), (c) `:With`에서 두 상류가 같은 소스에 다른 count를 들 때의 병합 + 규칙. **에이전트 권고는 (a) 전부 갱신 + (b) 첫 재계산 때 구성**이고, 그러면 + (c)는 자동 해소된다. 상세는 `base/state-epoch-plan.md`의 §5 7번. - **[해소, 2026-08-21] 공용 게이트 노드의 이름과 표면 — `state:Gate(setup)` 메소드 + `GateNode`로 확정.** 탑레벨 프리미티브는 안 만들고, `Blocker`는 `state:Block(blocker)` 안에서 그 배선을 쓴다(사용자: *"Gate 는 따로 diff --git a/.claude/reference/comparison-fusion-vide.md b/.claude/reference/comparison-fusion-vide.md index 8ce5252..5a8e21a 100644 --- a/.claude/reference/comparison-fusion-vide.md +++ b/.claude/reference/comparison-fusion-vide.md @@ -43,7 +43,8 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 근거로 인용될 때만 열 — quad Store가 이 naive BFS 방식을 그대로 베끼면 안 되는 이유. **[2026-08-14 보강]** quad가 이걸 피하는 방식은 "전파를 중간에 끊는 것"이 아니라 **애초에 push 시점에 계산을 안 하는 것**(pull-recompute + 노드별 - 캐시) — 신호는 두 경로로 두 번 도착해도 계산은 `:Get()` 때 한 번뿐. + 캐시) — 신호가 두 경로로 도착해도 계산은 `:Get()` 때 한 번뿐. + (**[2026-08-21]** 이제 신호 자체도 두 번 안 온다 — 아래 갱신 참고.) 즉 quad도 중복 *재평가*는 안 일어남 (`base/source-state-plan.md`의 "다이아몬드 의존성은 무엇이 푸는가" 절). **[2026-08-21 갱신]** 여기 있던 "중복 *통지*는 접지 않는다"는 뒤집혔다 — diff --git a/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md b/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md index db81441..3d0619f 100644 --- a/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md +++ b/.claude/session/2026-08-21-02-qa-round5-and-gate-epoch-research.md @@ -279,3 +279,18 @@ State 에포크도 **채택**했다(*"gate 와 epoch 가 제가 만족할만한 `rewrite-required/`로 갔다(다이아몬드 Observer가 이제 변경당 1회만 울어야 해서 핵심 assert가 정반대). 처리 전량은 `qa-request/pre-implementation-qa-round5-followup.md`의 O절. + +## 12. `/code-review high` — 12건 전부 유효, 그중 3건은 실제 설계 구멍 + +O절 커밋 직후 사용자가 돌린 리뷰에서 12건이 나왔고 전부 유효했다. 특히 +**게이트가 유보했다 내보내는 emit이 어느 source를 싣는가**는 M절이 이미 +짚어뒀는데(*"`nil` 규약만 Gate 설계와 같이 확정하면 된다"*) 표면 확정 때 +같이 안 닫힌 것으로, 그대로 두면 `blocker:Off()`의 배치 통지가 하류에서 +삼켜진다 — `setup` 시그니처에 영향이 있어 **M2 착수 전 항목**으로 되돌렸다 +(같은 날 "M2를 막는 항목 없음"이라 적었던 `todos.md` 00번도 정정). + +교훈이 하나 더 있다: 이번에도 **감사자 각도가 아니라 diff 각도에서만 보이는 +것**이 다수였다(개수 하드코딩, 배너 vs 본문, 같은 파일 안의 모순 문장, +방금 옮긴 파일을 가리키는 새 텍스트). `conventions.md`의 "`/code-review`는 +감사자를 대체하지 않는다" 항목이 다시 확인된 셈. 전량은 +`qa-request/pre-implementation-qa-round5-followup.md`의 P절. diff --git a/.claude/todos.md b/.claude/todos.md index 78a2487..99bec74 100644 --- a/.claude/todos.md +++ b/.claude/todos.md @@ -11,8 +11,15 @@ `state:Gate(setup)` + `GateNode`로(`base/gate-plan.md`), State의 재계산/전파 판정은 **소스 에포크 비교** 채택으로(`base/state-epoch-plan.md`, 구현은 M3) 닫혔다. 두 문서 모두 `research/`가 아니라 **`base/`**에 있다. - 남은 것은 판단이 아니라 구현 시 정할 것들 — `Gate`의 생명주기·재진입 - 계약, M2에 `Blocker`까지 넣을지, 그리고 스파이크 `05` 재작성 + **⚠️ [2026-08-21 정정 — 같은 날 `/code-review high`] "막는 항목이 없다"는 + 너무 이르다.** 게이트가 유보했다 내보내는 emit이 **어느 source를 싣는지**가 + 안 정해져 있고(확정된 `setup`은 source를 안 받는데 에포크 수신 규칙은 + `[source]` 키로 판정한다 — 그대로면 `blocker:Off()`의 배치 통지가 하류에서 + 삼켜진다), 이건 `setup` 시그니처를 바꿀 수 있어 **M2 착수 전 판단이 + 필요하다**(`question.md` 3번, `base/gate-plan.md`의 4번). M3 쪽으로는 두 + 맵의 초기값·병합·재계산 시 갱신 범위가 열려 있다(`state-epoch-plan.md` + §5 7번). 그 외 남은 것은 판단이 아니라 구현 시 정할 것들 — `Gate`의 + 생명주기·재진입 계약, M2에 `Blocker`까지 넣을지, 스파이크 `05` 재작성 (`luau-test/STATUS.md`). **4라운드 — [2026-08-21] 종결.** 문항지는