From c58c97a877d0099370d150746592916f5078b113 Mon Sep 17 00:00:00 2001 From: qwreey Date: Fri, 21 Aug 2026 21:04:27 +0900 Subject: [PATCH] =?UTF-8?q?design:=20Gate=20=ED=91=9C=EB=A9=B4=20=ED=99=95?= =?UTF-8?q?=EC=A0=95(state:Gate=20+=20GateNode)=20+=20State=20=EC=97=90?= =?UTF-8?q?=ED=8F=AC=ED=81=AC=20=EC=B1=84=ED=83=9D,=20=EB=91=90=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20base/=20=EC=8A=B9=EA=B2=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 확정 둘로 M2 착수를 막던 설계 항목이 전부 사라졌다. 1) Gate — 탑레벨 프리미티브를 만들지 않고 state:Gate(setup) 메소드로 확정. ComputeNode와 같은 층위의 GateNode를 만든다. Blocker는 그 위의 별개 프리미티브로, 이미 확정돼 있던 state:Block(blocker)가 내부에서 self:Gate(policy)를 부른다. Debounce/Throttle의 state:Apply(...) 관용구는 그대로 — 팩토리가 내부에서 :Gate를 부르면 되기 때문. 이름 문제(Gater?)도 메소드 자리로 가면서 소멸. Get()엔 영향 없음(통지만 막음)까지 확정. research/gate-primitive.md -> base/gate-plan.md. 2) State 에포크 — 채택 확정. 구현은 M3. research/state-epoch-validation.md -> base/state-epoch-plan.md. 에포크 채택으로 source-state-plan.md의 두 확정 서술("emit은 항상 전파" / "quad가 접지 않는 것은 중복 통지뿐")이 역전됐다. 원문은 archive/always-propagate-no-dedup-superseded.md. 지금 계약은 "invalid로는 절대 안 접고, 같은 소스의 같은 에포크가 두 번째로 도착했을 때만 접는다" — 2026-08-14의 invalid 기반 dedup 금지를 되돌린 게 아니라는 점을 역전 문서와 source-state-plan.md, README 세 곳에 못박음(흐려지면 "영구 침묵" 버그로 되돌아감). 같이 갱신: architecture.md 전파 모델 요약, blocker-plan.md(:Gate 배선 + Get 계약이 에포크 안의 전제라는 것), debounce-throttle-plan.md(공용 게이트 권고가 실현됨 / 파동 단위 최적화 서술 정정), reference/comparison-fusion-vide.md, source-state-plan.md의 Observer 계약 각주(이제 "새 에포크는 항상 통과"에 의존), ROADMAP M0 각주·M2 각주·M3 체크박스, README/question/todos 인덱스. 스파이크 05-store-state-diamond-propagation은 done/ -> rewrite-required/ 로 되돌렸다 — 다이아몬드 Observer가 이제 변경당 1회만 울어야 해서 핵심 assert가 정반대가 됐다(살릴 것/새로 넣을 것은 STATUS.md에 기재). 처리 전량의 소스는 qa-request/pre-implementation-qa-round5-followup.md의 O절. doc-check.py ERROR 0. Co-authored-by: qwreey Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D --- .claude/README.md | 7 +- .../always-propagate-no-dedup-superseded.md | 57 +++++++++++++++ .claude/base/architecture.md | 12 ++-- .claude/base/blocker-plan.md | 12 ++++ .claude/base/debounce-throttle-plan.md | 42 +++++++---- .claude/base/effect-plan.md | 2 +- .../gate-primitive.md => base/gate-plan.md} | 66 +++++++++++------ .claude/base/source-state-plan.md | 72 +++++++++++-------- .../state-epoch-plan.md} | 66 +++++++++-------- .claude/luau-test/README.md | 4 +- .claude/luau-test/STATUS.md | 8 +-- .../05-store-state-diamond-propagation.luau | 0 .../pre-implementation-qa-round5-followup.md | 47 ++++++++++++ .claude/question.md | 22 ++++-- .claude/reference/comparison-fusion-vide.md | 6 +- ...21-02-qa-round5-and-gate-epoch-research.md | 19 +++++ .claude/todos.md | 24 ++++--- ROADMAP.md | 37 ++++++---- 18 files changed, 362 insertions(+), 141 deletions(-) create mode 100644 .claude/archive/always-propagate-no-dedup-superseded.md rename .claude/{research/gate-primitive.md => base/gate-plan.md} (68%) rename .claude/{research/state-epoch-validation.md => base/state-epoch-plan.md} (82%) rename .claude/luau-test/{done => rewrite-required}/05-store-state-diamond-propagation.luau (100%) diff --git a/.claude/README.md b/.claude/README.md index d542d7e..5172c69 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-primitive.md`·`state-epoch-validation.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]** 그 회신 처리 결과 — 즉시 반영분 / 재질문 / 사용자 판단 필요 / 새로 만든 research 문서 둘(`gate-plan.md`·`state-epoch-plan.md`)까지. **처리 결과의 소스는 이 파일**). 다음 라운드가 필요해지면 라운드마다 파일을 새로 만들고 이름에 라운드 번호를 넣을 것 | | `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` | @@ -71,6 +71,8 @@ | `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)`는 `Tween` opts를 `T\|State`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) | | `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary" 절이 내린 "빈 자리 아님" 결론 위에 얹힘). `Fallback`은 `pcall` 기반(trace 없음), `Traceback`은 `xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 | | `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. **[2026-08-14 열 번째 세션]** `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고 `Observer`/`Effect`는 제외되는 쪽으로 확정되며 `OnDestroyed` 이름 재검토 조건이 발동 없이 종결 — `OnDestroyed`가 최종 이름, 용어 대기열에서도 제외 | +| `gate-plan.md` | **[2026-08-21 신설, 같은 날 표면 확정]** `state:Gate(setup)` — 상류 emit을 가로채 내려보낼지 정책이 정하는 **`GateNode`**(`ComputeNode`와 같은 층위)를 만드는 State 메소드. 탑레벨 `Gate(...)` 프리미티브는 **안 만든다**(처음 방향에서 뒤집힘) — `Blocker`가 `state:Block(blocker)` 안에서 이 배선을 쓰고, `Debounce`/`Throttle`은 `state:Apply(...)` 팩토리가 내부에서 `:Gate`를 부른다. `Get()`엔 영향 없음(통지만 막음)까지 확정. **남은 것은 생명주기·재진입 계약과 M2 범위**(`Gate`만 vs `Blocker`까지). 구현은 M2 | +| `state-epoch-plan.md` | **[2026-08-21 신설, 같은 날 채택 확정]** State의 재계산/전파 판정을 `invalid` 플래그가 아니라 **루트 Source 에포크 비교**로 한다 — DFS 전파 도중 `Get()`이 섞인 값을 캐시하던 glitch(실재)를 없애는 **정확성** 결정. 노드가 `sourceCountMap`(값 유효성)/`sourceEmitMap`(전파 dedup) 두 테이블을 들고, emit은 발행 source만 싣고, 순회는 `rawInvalid == false`일 때만 돌며 값만 앞당기고 통지는 상류 emit을 기다린다. 중복 *통지*도 같이 접히므로 `source-state-plan.md`의 옛 "항상 전파 / 중복 통지는 안 접음" 서술이 역전됨(`archive/always-propagate-no-dedup-superseded.md`). ⚠️ 2026-08-14에 폐기된 `invalid` 기반 dedup과는 다른 장치 — 그 금지는 유효. 구현은 M3 | ## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설) @@ -92,8 +94,6 @@ | `pre-implementation-audit.md` | M0 착수 직전 크리티컬 감사(2026-08-06 신설) — `base/` 전체를 모호성/지연결정리스크/단순화후보 세 렌즈로 재검토, 11개 우선순위1 + 11개 우선순위2 + 2개 단순화후보. **[2026-08-12 열일곱 번째 세션]** 우선순위1 11개 전원 해소 — 남은 건 `.claude/luau-test/` 스파이크 실측 확인뿐 | 상 — 설계는 전부 해소, `.claude/luau-test/` 스파이크 실측만 남음 | | `slot-attach-decomposition.md` | **[2026-08-21 신설, 같은 날 확정]** `attachSlot`의 책임 분해 근거 기록 — **결론은 후보 (B) 분해 채택**이고 `base/slot-plan.md`에 반영 완료(`materializeSlotTree`+`mountSlotTree`+두 줄짜리 `attachSlot` 래퍼). 아래는 그 논의의 출발점. QA 4라운드 `F-4-3`에서 `Dispatch.setLength`를 flush 앞/뒤 어디에 둘지가 갈렸는데, 사용자가 그건 자리 선택 문제가 아니라 *"attachSlot 의 기능이 너무 다양해진게 문제"* 라고 짚어 확장 논의로 넘어간 것. 지금 `attachSlot`이 지고 있는 책임 일곱(부모 등록 offset/length, `:List` 실체화, 마운트 상태 전이, 배치 게이팅, 자식 배치, 재귀)과 순서 제약 일곱(각각 `RC-1`/`RC-3`/`RC-4` 등 실제로 밟은 버그 출처까지)을 모아두고, **C6(길이 최종값은 flush 뒤에야 정해짐)와 C7(부기가 물리보다 먼저)이 단일 함수로는 동시 만족 불가능**함을 보인 뒤 분해 후보 넷(현행 유지 / `prepare`+`mount` 2단 / 3단 / 문서만)을 대조. 사용자 확정 논거는 *"지금 의사코드를 건들이는 비용이, 추후 실수가 누적되는 비용보다 싸다고 생각함"*. 정본은 여전히 `base/slot-plan.md` | 하 — **[2026-08-21] 결론 확정·반영 완료**, 이제 근거 기록용 | | `operator-sugar-plan.md` | **[2026-08-12 신설]** `Sum`/`Product`/`Not`/비트연산 등 `:Compute`/`:Apply`용 연산자 콤비네이터 슈가 — 메커니즘은 이미 확정된 계약(`Animate`와 동형 패턴) 재사용이라 확정, 네임스페이스 이름만 미정. **[2026-08-12 열아홉 번째 세션]** 서브 에이전트 외부 리서치로 다른 리액티브 라이브러리 선례와 대조 — `Operator`가 가장 강한 선례(Python `operator` 모듈), `Clamp`/`Min`/`Max`가 추가 후보로 부상, 비트연산·비교연산자·`Sub`/`Div`는 선례 전무로 드랍 후보, Debounce/Throttle은 `Blocker`와 다른 시간 기반 메커니즘이라 별도 질문으로 분리(**[2026-08-19]** 그 질문은 `base/debounce-throttle-plan.md`로 전부 해소·승격 완료), `Filtered`의 Slot 안/밖 구분 판단이 ReactiveUI/SolidJS 선례로 뒷받침됨 — 최종 이름 결정은 여전히 사용자 몫. **[2026-08-13 세션, 두 번째]** Haskell 비교 리서치 중 `Alternative`(nil 대체값, coalesce류) 후보 신설 — 카탈로그 확정 규칙에 그대로 맞음, 이전엔 없던 게 확인됨 | 하 — 구현은 맨 마지막(순수 슈가, 없어도 무방, 함수 간 의존 없음), 사용자가 직접 후순위 지정 **[2026-08-13 여섯 번째 세션]** `State|T>` → `State` 평탄화 항목 신설(백로그) — `State>`가 정상 동작하게 됐지만 `retractFrom`의 힌트가 직속 1단계에만 가서 깊은 중첩에선 깜빡임 방지가 꺼진다는 게 구체적 동기, 사용자 판단으로 "UB는 아니지만 원치 않는 방향". `Operator.*`가 아니라 `state:Flatten()` 메소드로 제공하는 게 맞아 보이며, **반환 노드가 동적 의존성을 갖는다는 난점**(quad가 의도적으로 비지원하기로 한 바로 그것)이 확정 전 최대 쟁점 | -| `gate-primitive.md` | **[2026-08-21 신설]** `Blocker`가 쓰는 게이티드 State 노드를 공용 `Gate`로 일반화 — 상류 emit을 가로채 내려보낼지 정책이 정하는 노드. `Blocker`/`Debounce`/`Throttle`이 그 위의 정책이 됨. **방향은 사용자 확정(게이팅을 M2로 앞당김), 이름(`Gate` vs `Gater`)·표면·`Blocker`와의 결합 방식이 미정** | **상 — M2 착수 전 필요**(`Dispatch.drive`의 배치 등록이 이 게이팅을 전제) | -| `state-epoch-validation.md` | **[2026-08-21 신설]** State 재계산 판정을 `invalid` 플래그에서 **루트 Source 에포크 비교**로 바꾸는 안(사용자 제안) — DFS 전파 도중 `Get()`이 섞인 값을 캐시하는 glitch를 없앰. 성능이 아니라 **정확성** 결정이고 State 내부 표현을 바꾸므로 M3 전에 결론 필요. 에이전트 분석: 진단·방향 모두 타당. **[같은 날 후속 확정] 중복 *통지*도 같은 장치로 접고**(emit이 발행 source를 실어오므로 판정 O(1)), "선언 안 된 의존성 UB 명문화"는 사용자 기각으로 빠짐. **[같은 날 3·4차 정정]** `rawInvalid` 기제가 뒤집혔고(순회는 `rawInvalid`가 **false**일 때만 돌고, emit은 count 없이 source만 싣는다), 순회가 발견한 변경의 처분이 **테이블 둘**로 확정됐다 — `sourceCountMap`(값 유효성)은 순회가 앞당겨 올리고 `sourceEmitMap`(전파 dedup)은 상류의 진짜 emit을 기다린다. 그래서 통지가 죽지도, 게이트를 새지도 않아 `source = nil` 규약이 불필요해짐. **기제는 사실상 다 정해졌고 남은 건 채택 여부 자체** | **상 — M3(State 구현) 착수 전 필요** | | `quad-recursive-acronym.md` | **[2026-08-14 신설]** GNU/WINE류로 `Quad`를 재귀 약어화하는 카피 브레인스토밍 — 설계 결정도 착수 게이팅도 아니고 나중에 README.md 헤딩 등에 쓸 캐치프레이즈 후보 모음. 자학 개그 방향(기각)과 지연평가/재귀·커링/펑터/클로저를 자랑하는 방향(채택 후보, 미확정) 정리 | 하 — 카피 소재, 설계 상의 필요 없음. 사용자가 최종 문구 고르면 반영 | | `v1-compat-plan.md` | v1 하위호환(compat) 레이어 — `quad-roblox-v1-compat` 패키지, v2→v1 단방향 브리지(`state:Observer()`+v1 프로퍼티 재대입), v2-in-v1/v1-in-v2 두 임베딩 방향의 기술 규칙까지 확정. quad2-try의 `quad-compat`은 빈 폴더로 실제 시도된 적 없었음을 확인 | 하 — Slot이 foreign Instance를 어떻게 다루는지만 Slot 코어 구현 시점까지 미결 | | `doc-include-plan.md` | **[2026-08-14 신설]** 문서 stale 감소용 include 도구 `doc-include.py`(가칭) — 원본 파일에 `` 류 마커로 요약 구간을 표시해두면 인용하는 문서가 그 구간을 기계적으로 추출해 붙여넣게 하는 도구. `doc-check.py`(사후 탐지)와 짝을 이루는 사전 차단 장치. AsciiDoc tagged include/markdown-magic이 선례, build vs buy 검토 후 Python 표준 라이브러리로 직접 제작(~100줄) 채택. 파일럿은 `.claude/session-summary.md` ← `.claude/session/*.md` 요약 마커부터(CLAUDE.md 분할로 목적지가 "통째로 생성되는 파일"이 돼 단방향 생성으로 단순화됨) | 하 — M0/설계 게이트와 무관한 메타 도구. **[2026-08-16 기준]** 플랜 초안 단계, 열린 질문 미해소(소스: 이 문서의 "열린 질문" 절) | @@ -104,6 +104,7 @@ | 문서 | 내용 | |---|---| +| `always-propagate-no-dedup-superseded.md` | **[역전됨, 2026-08-21 신설]** `source-state-plan.md`가 확정해뒀던 "emit은 자기 `invalid`와 무관하게 **항상** 전파된다 / quad가 접지 않는 것은 중복 *통지*뿐이다" — `base/state-epoch-plan.md` 채택으로 "같은 소스의 같은 에포크가 두 번째로 도착하면 접는다"로 바뀜. ⚠️ 2026-08-14의 `invalid` 기반 dedup 역전을 되돌린 게 아님(그 금지는 유효) | | `store-source-proxy-reversed.md` | [역전됨] 2026-08-04에 확정했던 `StoreSource` 프록시 설계(Store가 Source를 감춘 별도 프록시로 감쌈) — 2026-08-06 세 번째 세션에서 "Source가 State를 구조적으로 만족" 재구성으로 완전히 대체됨. 원문·역전 이유·신구 비교표 보존, `quadnomicon` 소재 후보 | | `ref-phase-option-reversed.md` | [역전됨] `CreatedRef`의 `phase` 옵션 — 위치 기반 순서 + `PreRef` 신설로 대체됨 | | `preref-order-unguaranteed-withdrawn.md` | **[철회됨, 2026-08-14 아홉 번째 세션 신설]** 복수 `PreRef`/`PostRef` 간 fire 순서를 "배열 index 순서 보장"에서 **미보장으로 바꾸려던 안** — 같은 세션에 제안·철회, 현재 계약은 **보장**(2026-08-07 결정 그대로). 반례는 `FastQuery(...) -> PreRef`류 조합(앞자리 항목이 뒤 항목의 전제를 만들어주는 정당한 합성), 보장 비용 0 + 배열 파트 index 순서 계약의 자동 귀결이라 새로 내주는 자유도 없음. 양쪽 논거 보존 | diff --git a/.claude/archive/always-propagate-no-dedup-superseded.md b/.claude/archive/always-propagate-no-dedup-superseded.md new file mode 100644 index 0000000..0fbe913 --- /dev/null +++ b/.claude/archive/always-propagate-no-dedup-superseded.md @@ -0,0 +1,57 @@ +# [역전됨, 2026-08-21] "emit은 항상 전파된다 / 중복 통지는 접지 않는다" + +**무엇이 뒤집혔나**: `base/source-state-plan.md`의 "전파 모델 확정" 절과 +"다이아몬드 의존성은 무엇이 푸는가" 절이 확정해두던 두 서술 — +(1) emit은 자기 `invalid` 상태와 무관하게 **항상** 전파된다, (2) quad가 추가로 +접지 않는 것은 **중복 통지**뿐이다. + +**무엇으로 바뀌었나**: `base/state-epoch-plan.md`(소스 에포크 비교)를 채택하면서 +**같은 소스의 같은 에포크가 두 번째로 도착하면 접는다**로 바뀌었다. 다이아몬드에서 +값도 통지도 한 번씩만 간다. + +**⚠️ 2026-08-14의 역전을 되돌린 게 아니다.** 그때 폐기된 +(`archive/invalidate-dedup-propagation-reversed.md`) 것은 *"이미 `invalid`면 더 안 +내려보낸다"*는 **플래그 기반** dedup이고, 그건 `:Get()`을 안 부르는 Observer가 +**한 번 울고 영구히 침묵**하는 실패 모드가 있었다. 에포크 비교엔 그 모드가 없다 — +매 `Set`마다 카운트가 새 값이라 항상 통과하고, 접히는 건 같은 에포크가 두 경로로 +도착한 두 번째뿐이다. **`invalid`로 접는 것은 지금도 금지**다. + +**역전 이유**: 플래그가 아니라 에포크로 판정하면 (a) DFS 전파 도중 `Get()`이 +섞인 값을 캐시하는 glitch가 없어지고, (b) 그 판정이 중복 통지까지 O(1)로 접는다. +성능이 아니라 **정확성** 동기다 — 경위는 `base/state-epoch-plan.md` §1·§3. + +--- + +## 역전 전 원문 (2026-08-14~2026-08-21) + +### "전파 모델 확정" 절에서 + +> - **⭐ emit(무효화 신호)은 구독자에게 *항상* 전파된다 — 자기 `invalid` +> 상태와 무관.** 신호를 받은 State는 자기 `invalid` 플래그를 세우고, +> **이미 `invalid`였더라도 그대로 아래로 전파한다.** +> - **`invalid` 플래그의 역할은 "내 캐시가 낡았다"는 표시 하나뿐** — +> 전파를 제어하는 장치가 **아님**. +> - **emit 전파를 늦추거나 흡수할 수 있는 건 명시적인 게이트 요소뿐** — +> 지금은 `Blocker`가 유일하고 … **평범한 State는 절대 신호를 삼키지 +> 않는다.** + +### "다이아몬드 의존성은 무엇이 푸는가" 절에서 + +> 즉 **중복 재계산을 막는 주체는 pull-recompute + 캐시**이지, 전파를 +> 중간에 끊는 게 아니다. Vide가 `todo.md`에 미해결로 남긴 "다이아몬드 중복 +> **재평가**"는 이 캐시 구조로 풀리고, quad가 추가로 접지 않는 것은 +> **중복 *통지***뿐이다 — `d` 아래의 `Observer`는 한 사이클에 두 번 울 수 +> 있고, 그건 의도된 동작이다(통지를 접으려면 `Blocker` 같은 명시적 게이트를 +> 쓸 것). +> +> - **순회 비용이 실측에서 문제가 되면** 그때 "**한 번의 전파 파동 +> 안에서만** 같은 노드를 두 번 방문하지 않는다"는 최적화를 넣을 수 있음 +> (방문 집합/에포크 카운터). 단 그건 **파동 단위**여야 하고, 지금처럼 +> 시간에 걸쳐 유지되는 `invalid` 플래그로 하면 안 됨 … 이 최적화는 +> 의미론에 안 보이는 순수 구현 사항이라 지금 결정할 필요 없음. + +**마지막 문단에 대한 주석**: "파동 단위여야 한다"는 경고는 **`invalid` 플래그로 +하지 말라**는 뜻이었고, 채택된 에포크 모델은 시간에 걸쳐 유지되는 카운트를 쓰지만 +**소스별 에포크**라 그 함정에 안 걸린다(새 `Set`은 새 에포크). 또 이 최적화는 +"의미론에 안 보이는 순수 구현 사항"이 아니었다 — 실제로는 `Get()`이 섞인 값을 +돌려주던 것을 고치는 **의미론 변경**이었다. diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 859bf72..91d5df0 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -449,10 +449,14 @@ push-invalidate(신호만)/ pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림(**[2026-08-14 보강]** 푸는 주체는 **노드별 캐시**임을 명시 — `invalid`는 "내 캐시가 낡았다" 표시일 뿐이고 -**emit 전파는 자기 `invalid` 상태와 무관하게 항상 일어남**. 전파를 늦추는 -건 `Blocker` 같은 명시적 게이트뿐. 한때 `source-state-plan.md`가 "이미 -`invalid`면 전파 중단"으로 서술했으나 `Observer` 계약과 모순돼 역전됨 — -`archive/invalidate-dedup-propagation-reversed.md`). State는 쓰기 대상이 아니고, 값을 쓰는 +**emit 전파는 자기 `invalid` 상태와 무관함**. 한때 `source-state-plan.md`가 +"이미 `invalid`면 전파 중단"으로 서술했으나 `Observer` 계약과 모순돼 역전됨 — +`archive/invalidate-dedup-propagation-reversed.md`. **[2026-08-21 갱신]** +전파를 접는 판정은 이제 `invalid`가 아니라 **소스 에포크 비교**가 한다 — +같은 소스의 같은 에포크가 두 경로로 도착하면 두 번째는 접히고(다이아몬드에서 +값도 통지도 한 번), DFS 도중 `Get()`이 섞인 값을 캐시하던 glitch도 같이 +사라진다. 규칙 전량은 `base/state-epoch-plan.md`, 명시적 게이트는 +`base/gate-plan.md`(`state:Gate`)와 그 위의 `Blocker`). State는 쓰기 대상이 아니고, 값을 쓰는 경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 — `:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가; [정정, 2026-08-07] 읽기는 `:Get()` 하나로 통일 — 프로퍼티 읽기 표기는 Ref의 `.Value` 전용으로 좁혀짐. **[표기 정정, 2026-08-18]** 여기 소문자 `.value`로 적혀 있었음). 값 하나만 diff --git a/.claude/base/blocker-plan.md b/.claude/base/blocker-plan.md index 3732703..26587dd 100644 --- a/.claude/base/blocker-plan.md +++ b/.claude/base/blocker-plan.md @@ -67,9 +67,21 @@ gated state의 동작: `OffWithoutEmit()`은 "밀린 전파를 버리며 끈다" — 어느 쪽이든 대기 상태(`HasBlockedEmit`)는 항상 깨끗하게 리셋됨. +**⭐ [2026-08-21 신설] `state:Block(blocker)`는 `state:Gate(setup)` 위에 +얹힌다.** 위 "gated state의 동작"은 `Blocker`만의 특수 노드가 아니라 +`base/gate-plan.md`가 확정한 **`GateNode`**(`ComputeNode`와 같은 층위)의 +정책 하나다 — `Block`이 내부에서 `self:Gate(policy)`를 부르고, 그 `policy`가 +`blocker.IsBlocked`를 보고 `emit()`을 부를지 `HasBlockedEmit`만 세울지 +정한다. `Debounce`/`Throttle`도 같은 자리에 다른 정책으로 들어간다. +**공개 표면(`Blocker()` 생성자와 `state:Block`)은 안 바뀐다** — 배선만 +공용 노드를 쓰는 것. + **`:Get()`엔 영향 없음** — 블록은 emit **전파**만 지연시킨다. 블록 중이라도 누군가 명시적으로 `:Get()`하면 그 순간의 실제 값을 정상적으로 계산해서 준다 — `base/source-state-plan.md`의 "Source 값을 직접 mutate한 뒤 전파 — `:Emit()`" 절("`Get()`은 라이브 레퍼런스를 준다" 캐비엇)과 일치. +**[2026-08-21] 이 계약은 `base/state-epoch-plan.md`가 의존하는 전제다** — 그 +문서 §5의 3번이 "게이트를 에포크 경계로 만드는" 대안을 기각한 이유가 정확히 +이걸 뒤집지 않기 위해서다. 바꾸려면 그쪽도 같이 봐야 한다. ## 사용 예시 diff --git a/.claude/base/debounce-throttle-plan.md b/.claude/base/debounce-throttle-plan.md index cf64f31..953a397 100644 --- a/.claude/base/debounce-throttle-plan.md +++ b/.claude/base/debounce-throttle-plan.md @@ -194,11 +194,17 @@ leading/trailing/통과 후 창 재개방은 **완전히 동일**. 그래서 공 타이머를 (재)시작하려면 그 순간을 알아야 하는데 알 방법이 없어서, 결국 게이트 노드 내부 훅이 필요함. -**그래서 권하는 구현 방향**: `Blocker`의 게이티드 노드를 내부 공용 -`Gate` 노드로 한 겹 일반화하고(= "상류 신호를 받되 전파 여부를 정책이 -결정하는 State 노드"), `Blocker`/`Debounce`/`Throttle`이 그 위의 서로 다른 -정책으로 얹히는 형태. 새 노드 종류를 하나 더 만드는 게 아니라, 이미 하나 -있는 걸 두 번째 사용처가 생겼으니 이름 붙여 꺼내는 것뿐. +**그래서 권하는 구현 방향**: `Blocker`의 게이티드 노드를 공용 게이트 노드로 +한 겹 일반화하고(= "상류 신호를 받되 전파 여부를 정책이 결정하는 State 노드"), +`Blocker`/`Debounce`/`Throttle`이 그 위의 서로 다른 정책으로 얹히는 형태. +새 노드 종류를 하나 더 만드는 게 아니라, 이미 하나 있는 걸 두 번째 사용처가 +생겼으니 이름 붙여 꺼내는 것뿐. + +**[2026-08-21 실현 — 이 권고가 확정됐다]** 그 노드는 `state:Gate(setup)`가 +만드는 **`GateNode`**이고 **M2**에서 구현된다(`base/gate-plan.md`). 다만 +"내부 공용"은 아니게 됐다 — **공개 표면**이다. `Debounce`/`Throttle` 쪽 +관용구는 안 바뀐다: `Debounce{...}`가 돌려주는 팩토리가 내부에서 +`s:Gate(policy)`를 부르므로 `state:Apply(Debounce{...})`가 그대로 성립한다. --- @@ -287,14 +293,20 @@ quad의 전파 모델은 `base/source-state-plan.md`의 "전파 모델 확정" ### 그래서 정정 후 그림 (권고) -- **emit(무효화 신호)은 구독자에게 항상 전파된다.** State가 이미 - `invalid`인지는 전파 여부와 무관. +- **emit(무효화 신호)은 구독자에게 전파된다.** State가 이미 `invalid`인지는 + 전파 여부와 무관. **[2026-08-21 갱신]** "항상"은 빠졌다 — 소스 에포크 + 비교를 채택하면서 **같은 에포크가 두 번째로 도착하면 접힌다** + (`base/state-epoch-plan.md`). `invalid`로 접는 것이 금지인 건 그대로. - 다이아몬드 중복 *재계산*은 **pull-recompute가 이미 구조적으로** 막음(`architecture.md`의 서술이 맞음) — 별도 장치 불필요. -- 한 번의 변경이 여러 경로로 같은 노드에 닿는 **파동 내 중복 순회**가 +- ~~한 번의 변경이 여러 경로로 같은 노드에 닿는 **파동 내 중복 순회**가 실측에서 문제가 되면, 그때 **파동 단위**(방문 집합/에포크 카운터)로 접으면 됨 — 이건 의미론에 안 보이는 **순수 구현 최적화**이고, 지금처럼 - 영속 플래그로 하면 안 됨. + 영속 플래그로 하면 안 됨.~~ **[2026-08-21 실현·정정]** 실제로 채택된 건 + 파동 단위가 아니라 **소스별 영속 에포크**이고(그래서 "영속이면 안 된다"는 + 경고는 `invalid` 플래그에만 해당했던 것으로 좁혀짐), 순수 최적화도 아니라 + `Get()`이 섞인 값을 돌려주던 것을 고치는 **의미론 변경**이었다 — + `base/state-epoch-plan.md`. ### 이 문서에 미치는 영향 — 세 개가 사라짐 @@ -318,6 +330,11 @@ quad의 전파 모델은 `base/source-state-plan.md`의 "전파 모델 확정" > 위로 올라가서 받아와서 계산 처리된 게 들어오고 cache가 쓰인 다음 > `invalid`가 꺼짐. +**[2026-08-21 후속]** 위는 2026-08-14 시점 확정 원문이다. 그 뒤 소스 에포크 +비교를 채택하면서 "항상"에 예외가 하나 생겼다 — **같은 소스의 같은 에포크가 +두 번째로 도착하면 접힌다**(`base/state-epoch-plan.md`). `invalid`로 접는 것이 +금지라는 이 문단의 요지는 그대로다. + 그리고 "다 다시 써야 한다"는 지시로 **코퍼스 전체 정정을 같은 세션에 수행함**. 정정된 모델: @@ -916,8 +933,9 @@ function Throttle(opts: ThrottleOptions) return makeGate(false, opts) end **(A) emit-gate가 붙는 자리(확정, 4절)**: `gate`는 다른 평범한 State와 똑같이 **매 `onUpstreamSignal` 진입 시 자기 `invalid`를 즉시 세운다** — -`source-state-plan.md`의 "전파 모델 확정" 절이 정한 "emit은 항상 전파된다" -규칙이 게이트 자신에게도 그대로 적용됨. 위 코드의 `gate:passThrough()`가 +`source-state-plan.md`의 "전파 모델 확정" 절이 정한 전파 규칙(**[2026-08-21]** +"항상"이 아니라 "같은 에포크의 두 번째만 접는다" — `base/state-epoch-plan.md`)이 +게이트 자신에게도 그대로 적용됨. 위 코드의 `gate:passThrough()`가 실제로 미루는 건 **다운스트림 통지(전파)뿐**이지 invalid 세팅이 아님 — 그래서 창이 열려 있는 동안 `gate:Get()`을 불러도 항상 최신값이 계산됨(캐시가 stale한 @@ -1106,6 +1124,6 @@ additional-primitives-plan.md`가 원래 "안 만들어도 된다"고 판단했 "게이팅 먼저"(`Dispatch.drive`의 배치 등록이 이미 그 게이팅에 의존하므로). 1절에서 봤듯 같은 노드를 공유하므로 따로 하면 같은 걸 두 번 설계하게 되는 것은 그대로이고, 바뀐 건 **언제**뿐이다. 표면/이름은 아직 미정 — -`research/gate-primitive.md`가 소스(이 문서의 1절이 그 일반화를 처음 +`base/gate-plan.md`가 소스(이 문서의 1절이 그 일반화를 처음 권고한 자리로 거기 인용돼 있다). 프리미티브 자체(`Debounce`/`Throttle` 함수)는 그 위에 아무 때나 나중에 얹으면 된다. diff --git a/.claude/base/effect-plan.md b/.claude/base/effect-plan.md index 5a94d95..1defe8c 100644 --- a/.claude/base/effect-plan.md +++ b/.claude/base/effect-plan.md @@ -261,7 +261,7 @@ Effect의 의존성이 될 방법이 아예 없다.** 사용자 제기: *"Effect 즉시 1회 실행"이 N번 발화하므로, 설치 구간 동안 발화를 눌러뒀다가 마지막에 한 번만 실행한다 — **`Blocker`의 "`state:Block()` 없이 직접 쓰는" 용례**를 그대로 재사용(`base/blocker-plan.md`). **⚠️ 이 억제 장치의 정확한 모양은 - `Gate`(공용 게이트 노드) 설계에 딸려 있다** — `research/gate-primitive.md`가 + `Gate`(공용 게이트 노드) 설계에 딸려 있다** — `base/gate-plan.md`가 닫힌 뒤에 확정할 것. - **leaf dedup/cascade가 전부를 덮어야 한다** — 의존성이 N개면 내부 Observer도 N개라, `EffectHandle`의 bind/unbind cascade와 dedup 분기가 **그 전부**를 diff --git a/.claude/research/gate-primitive.md b/.claude/base/gate-plan.md similarity index 68% rename from .claude/research/gate-primitive.md rename to .claude/base/gate-plan.md index 791415c..231e7d2 100644 --- a/.claude/research/gate-primitive.md +++ b/.claude/base/gate-plan.md @@ -1,15 +1,19 @@ -# `Gate` — emit을 가로채는 공용 게이트 노드 (2026-08-21 신설) +# `Gate` — emit을 가로채는 게이트 노드 (2026-08-21 확정) -**상태**: research — **방향은 사용자 확정, 정확한 표면이 미정(단 [2026-08-21] -"`:Apply`가 아니라 State 메소드"까지는 확정됨 — 아래 2번). -[2026-08-21] 사용자 지시로 설계 자체는 다음 세션으로 미룸** — *"고칠것이 많으므로 -Gate 는 다음 세션에 다루겠음. 해당 부분은 정정이 아니고 추가이고, 새 인터페이스를 -고민해야하므로 해결해야할 일로 남겨두길 바람. 단지 지금 세션 상 지식만 이전될 수 -있게 두세요."* 그래서 이 문서는 **다음 세션이 바로 이어받을 수 있게 재료만** -모아둔 상태다(아래 "아직 안 정한 것"이 그 목록). 구현 전 QA -5라운드(`CR-3`/`DT-4`)에서 "M2 전에 게이팅부터 만들 준비를 하고, 실질적 모양을 -정의해야 한다"는 사용자 결정이 나와 신설. 회신 원문은 -`qa-request/pre-implementation-qa-round5-response.md`. +**상태**: **표면 확정.** `Blocker`/`Debounce`/`Throttle`이 공유하는 게이팅 +메커니즘을 `state:Gate(setup)` **메소드**로 확정했다 — *"Gate 는 따로 프리미티브 +없이 `state:Gate( (emit) -> ()->() )` 처럼 선언되고 마치 Compute 처럼 +GateNode(ComputeNode 처럼) 생성된다 그리고 Blocker 는 해당 내부 배선을 따른다 +← 동의합니다 해당 방법대로 확정하면 됩니다."* 구현은 **M2**("게이팅 먼저" +결정, `ROADMAP.md`). **남은 것은 생명주기·재진입 계약과 M2 범위뿐**(아래 +"아직 안 정한 것"). + +**⚠️ 처음 방향이 한 번 바뀌었다.** 신설 당시엔 *"공용 `Gate` 프리미티브를 꺼내고 +`Blocker`가 그걸 컴포지션한다"*였는데, 확정된 형태는 **프리미티브를 따로 안 +만들고 State 메소드 하나로 끝낸다**이다 — *"Gate 프리미티브를 만들고 Blocker 가 +컴포지션 하는걸 생각했는데, 그럴 필요가 없네요. `:Gate` 는 또 Apply 에서 쓸만한 +표면을 주기도 하구요."* 아래 본문 중 "공개 프리미티브로 꺼낸다"류 서술은 그 +이전 시점 표현이니 이 배너 기준으로 읽을 것. **한 줄**: `Blocker`가 쓰는 "게이티드 State 노드"를 한 겹 일반화해서, **상류 emit을 가로채 내려보낼지 말지를 정책이 정하는** 노드를 공개 프리미티브로 꺼낸다. @@ -55,15 +59,27 @@ end) ## 아직 안 정한 것 (사용자 판단 필요) -1. **⭐ 이름.** 사용자 지적: *"프리미티브 명을 Gater? 뭔가 이상하게 들어간다는게 - 약간의 문제."* — 코퍼스 관례가 `Blocker`/`Modifier`/`Observer`처럼 `-er`가 - 많아서 형태만 맞추면 `Gater`인데 영어로 어색하다. - **에이전트 권고: `Gate` 그대로.** `blocker`/`modifier`와 달리 `gate`는 이미 - **행위자가 아니라 장치를 가리키는 명사**라 `-er`를 붙일 이유가 없다 - (`Source`/`Ref`/`Slot`/`Tween`도 전부 `-er` 없는 명사). 대안 후보: - `Valve`(밸브 — 흐름 제어라는 뜻은 더 정확하지만 코퍼스 어휘와 멀다), - `Relay`(전기 릴레이 — "받아서 다시 보낸다"는 뜻은 맞으나 "중계"로 오독 여지). +1. **[2026-08-21 해소] 이름 — `Gate`.** 원래 걸림돌은 *"프리미티브 명을 + Gater? 뭔가 이상하게 들어간다"*였는데, **탑레벨 프리미티브를 안 만들기로 + 하면서 문제 자체가 사라졌다** — 이름이 놓이는 자리가 `Blocker()` 같은 + 생성자가 아니라 `state:Gate(...)` **메소드**이고, 메소드 자리에서 `Gate`는 + `:With`/`:Compute`/`:Observer`와 나란히 자연스럽다. 노드 타입 이름은 + `ComputeNode`와 짝을 맞춘 **`GateNode`**. 2. **[2026-08-21 해소] `:Apply`가 아니라 State의 메소드다 — `state:Gate(setup)`.** + **확정 형태**: + + ```lua + -- setup: (emit: () -> ()) -> (onUpstreamEmit: () -> ()) + local gated = state:Gate(setup) -- ComputeNode처럼 GateNode를 하나 만든다 + ``` + + `Blocker`는 **그 위에 얹히는 별개 프리미티브**로, `state:Block(blocker)`가 + 내부에서 이 배선을 그대로 쓴다. 탑레벨 `Gate(...)` 생성자는 **안 만든다.** + `:Gate`가 메소드라고 `:Apply`와 배타적인 것도 아니다 — 사용자 지적대로 + *"`:Gate` 는 또 Apply 에서 쓸만한 표면을 주기도"* 하므로, + `Debounce{...}` 같은 유저랜드 팩토리가 내부에서 `s:Gate(policy)`를 부르는 + 형태로 `state:Apply(Debounce{...})` 관용구가 그대로 성립한다. + **사용자 확정**: *"gate 는 apply 불가하다고 판단함. 순수 슈가가 아니기 때문, state 의 전파를 손대는 작업이라 with 처럼 다른 노드가 나는게 맞음."* - **정확한 경계**: `Apply`는 `factory(self)`일 뿐이라(`base/source-state-plan.md`의 @@ -89,10 +105,14 @@ end) **그 노드의 `emit`을 캡처**하고 반환 클로저가 상류 emit마다 정책을 태운다. `Blocker`처럼 **여러 노드가 공유하는 정책**은 공유 상태를 바깥 객체가 들고, 노드별 `emit`만 2단 구조로 받아 등록하면 된다. -3. **`Get()`과의 관계.** `Blocker`는 `:Get()`에 영향이 없고(`base/blocker-plan.md`), - `Debounce`/`Throttle`도 emit-gate로 확정됐다(`base/debounce-throttle-plan.md` §4). - `Gate`도 **값이 아니라 통지만 막는다**로 통일하는 게 맞는지 확인 필요 — - 맞다면 "게이트를 통과하지 않은 값도 `:Get()`으로는 보인다"가 공개 계약이 된다. +3. **[2026-08-21 확정] `Get()`과의 관계 — 값이 아니라 통지만 막는다.** + `Blocker`가 이미 그렇고(`base/blocker-plan.md`의 "`:Get()`엔 영향 없음"), + `Debounce`/`Throttle`도 emit-gate로 확정돼 있어(`base/debounce-throttle-plan.md` + §4) `Gate`도 같은 계약으로 통일한다. 공개 계약 문구: **"게이트를 통과하지 + 않은 값도 `:Get()`으로는 보인다."** + `base/state-epoch-plan.md`가 이 계약에 **의존**한다 — 그 문서 §5의 3번이 + "게이트를 에포크 경계로 만드는" 대안을 기각한 이유가 정확히 이 계약을 + 뒤집지 않기 위해서다. 4. **생명주기.** 게이트 노드가 잡는 자원(타이머/플래그)이 언제 죽는가 — 지금 설계대로면 다운스트림이 다 죽으면 GC(팩토리는 weak 추적, `debounce-throttle-plan.md` 5-4). `Gate` 자체에 `Flush`/`Cancel` 같은 표면을 diff --git a/.claude/base/source-state-plan.md b/.claude/base/source-state-plan.md index 70a6c4f..ac167b5 100644 --- a/.claude/base/source-state-plan.md +++ b/.claude/base/source-state-plan.md @@ -187,23 +187,27 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa - `Source`는 값이 바뀌면 구독 중인 State들에게 **"무효화됐다"는 신호만 쏜다** — 새 값 자체는 신호에 안 실림("state는 세터를 내보내기보다 업데이트 됐다는 신호만 쏜다" — 사용자 확정 문구). -- **⭐ emit(무효화 신호)은 구독자에게 *항상* 전파된다 — 자기 `invalid` - 상태와 무관.** 신호를 받은 State는 자기 `invalid` 플래그를 세우고, - **이미 `invalid`였더라도 그대로 아래로 전파한다.** - - **`invalid` 플래그의 역할은 "내 캐시가 낡았다"는 표시 하나뿐** — - 전파를 제어하는 장치가 **아님**. `:Get()`이 호출되면 상류로 올라가 - 재계산하고, 그 결과를 캐시에 넣고, `invalid`를 끈다. - - **emit 전파를 늦추거나 흡수할 수 있는 건 명시적인 게이트 요소뿐** — - 지금은 `Blocker`(`base/blocker-plan.md`)가 유일하고, - `base/debounce-throttle-plan.md`의 시간 기반 게이트가 같은 자리에 - 들어감(설계 확정, 구현은 아직). **평범한 State는 절대 신호를 삼키지 - 않는다.** - - **[2026-08-14 정정 — 중요]** 이 자리엔 원래 "이미 `invalid`였다면 그 - 아래로 더 전파하지 않는다(다이아몬드 중복 워크 방지)"라고 적혀 있었으나 - **틀린 서술이라 뒤집힘**. 그대로 두면 `:Get()`을 호출하지 않는 - `Observer`(아래 "`state:Observer(fn)`" 절이 명시적으로 허용하는 사용법)가 - **한 번 울고 영구히 침묵**하게 됨. 원문·역전 근거·영향 범위는 - `archive/invalidate-dedup-propagation-reversed.md` 참고. +- **⭐ [2026-08-21 재확정 — 판정 주체가 `invalid`에서 소스 에포크로 바뀜] + emit은 자기 `invalid` 상태와 **무관하게** 전파되고, 접히는 것은 오직 + **같은 소스의 같은 에포크가 두 번째로 도착했을 때**뿐이다.** 판정 규칙 + 전량은 **`base/state-epoch-plan.md`가 소스** — 여기선 이 절의 다른 서술과 + 어긋나지 않게 요지만 적는다. + - **`invalid`(구현 이름 `rawInvalid`) 플래그의 역할은 "내 캐시가 낡았다"는 + 표시 하나뿐** — 전파를 제어하는 장치가 **아님**. `:Get()`이 호출되면 + 상류로 올라가 재계산하고, 그 결과를 캐시에 넣고, 플래그를 끈다. + - **⚠️ `invalid`로 전파를 접는 것은 지금도 금지다.** 2026-08-14에 그 + 방식이 폐기된 이유는 `:Get()`을 호출하지 않는 `Observer`(아래 + "`state:Observer(fn)`" 절이 명시적으로 허용하는 사용법)가 **한 번 울고 + 영구히 침묵**하기 때문이고, 그 실패 모드는 지금도 유효하다(원문은 + `archive/invalidate-dedup-propagation-reversed.md`). 에포크 비교엔 그 모드가 + 없다 — 매 `Set`이 **새 에포크**라 항상 통과하고, 접히는 건 **다이아몬드에서 + 같은 에포크가 두 경로로 도착한 두 번째**뿐이다. + - **emit 전파를 늦추거나 흡수할 수 있는 건 위 dedup 외엔 명시적인 게이트 + 요소뿐** — `state:Gate(setup)`(`base/gate-plan.md`)와 그 위에 얹히는 + `Blocker`(`base/blocker-plan.md`), `base/debounce-throttle-plan.md`의 시간 + 기반 정책. **평범한 State는 그 외의 이유로 신호를 삼키지 않는다.** + - **[2026-08-21] 여기 있던 "emit은 *항상* 전파된다"는 무조건 서술은 + 역전됐다** — 원문은 `archive/always-propagate-no-dedup-superseded.md`. - 실제 재계산은 `:Get()`이 호출되는 시점에만 일어남 — "필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/ 생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한 @@ -213,7 +217,7 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa State 스스로 "지금 나를 보는 eager 소비자가 있나" 같은 부기가 전혀 필요 없음. - `emit`은 이 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함 - ("emit 필요 여부" 열린 질문은 이걸로 해소). 항상 전파해도 부담이 작은 + ("emit 필요 여부" 열린 질문은 이걸로 해소). 전파가 잦아도 부담이 작은 이유이기도 함 — 신호 하나가 트리를 훑는 비용이지 재계산 비용이 아님. ### 다이아몬드 의존성은 무엇이 푸는가 (2026-08-14 명확화) @@ -231,18 +235,23 @@ Handler가 애초에 다른 층위. 관련해서 Handler를 담는 엔진(`Dispa 그대로 반환**. 즉 **중복 재계산을 막는 주체는 pull-recompute + 캐시**이지, 전파를 -중간에 끊는 게 아니다(`base/architecture.md`의 전파 모델 요약과 같은 +`invalid`로 끊는 게 아니다(`base/architecture.md`의 전파 모델 요약과 같은 이야기). Vide가 `todo.md`에 미해결로 남긴 "다이아몬드 중복 **재평가**"는 -이 캐시 구조로 풀리고, quad가 추가로 접지 않는 것은 **중복 *통지***뿐이다 -— `d` 아래의 `Observer`는 한 사이클에 두 번 울 수 있고, 그건 의도된 -동작이다(통지를 접으려면 `Blocker` 같은 명시적 게이트를 쓸 것). +이 캐시 구조로 풀린다. -- **순회 비용이 실측에서 문제가 되면** 그때 "**한 번의 전파 파동 - 안에서만** 같은 노드를 두 번 방문하지 않는다"는 최적화를 넣을 수 있음 - (방문 집합/에포크 카운터). 단 그건 **파동 단위**여야 하고, 지금처럼 - 시간에 걸쳐 유지되는 `invalid` 플래그로 하면 안 됨 — 그게 위 정정의 - 핵심. 이 최적화는 의미론에 안 보이는 순수 구현 사항이라 지금 결정할 - 필요 없음. +**⭐ [2026-08-21 역전] 중복 *통지*도 이제 접힌다.** 여기엔 원래 "quad가 추가로 +접지 않는 것은 중복 통지뿐이고, `d` 아래 `Observer`가 한 사이클에 두 번 우는 +것은 의도된 동작"이라고 적혀 있었으나, `base/state-epoch-plan.md` 채택으로 +**두 번째 신호는 삼켜진다** — 그 신호가 나르는 에포크를 `d`가 이미 봤기 +때문이다. 그래서 위 1단계는 "`d`가 두 번 받는다"가 아니라 **"두 번째는 +`d`에서 멈춘다"**가 된다. 역전 원문과 이게 2026-08-14 역전을 되돌린 게 아닌 +이유는 `archive/always-propagate-no-dedup-superseded.md`. + +**부수로 같이 고쳐진 것 — 섞인 값(glitch).** 옛 모델에선 전파가 DFS라 +`b` 가지가 먼저 끝까지 내려가고, 그 아래 Observer가 `d:Get()`을 부르면 `c`는 +아직 신호를 못 받아 **옛 캐시를 반환**해 `d`가 `(b_new, c_old)`를 캐시했다. +에포크 비교는 `c`가 신호 없이도 스스로 낡음을 알아채므로 이 창이 없다 — +상세와 재현 시나리오는 `base/state-epoch-plan.md` §1. **전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)** @@ -921,8 +930,11 @@ retract/Destroy되면 자동으로 정리됨. `:With`한 값에 따라 갈리는 경우가 있어서(위 "포지셔널 인자 지양" 절의 `noprint` 예시처럼 계산 자체를 통째로 생략하고 싶을 수 있음) — `Get()` 호출 여부를 작성자가 직접 결정하게 열어둔 것. - - **⚠️ 이 허용이 전파 모델의 "emit은 항상 전파된다"에 의존한다 - (2026-08-14 명시).** `fn`이 `:Get()`을 안 하면 상류 State는 계속 + - **⚠️ 이 허용이 전파 규칙에 의존한다(2026-08-14 명시, + [2026-08-21 갱신]).** 지금 형태로 말하면 **"`Set` 한 번은 새 에포크라 + 항상 통과한다"**에 의존한다 — 에포크 dedup이 접는 건 *같은* 에포크의 + 두 번째 도착뿐이라 이 Observer는 매 변경마다 정확히 한 번 운다 + (`base/state-epoch-plan.md`). 아래 서술의 "항상 전파"는 그 뜻으로 읽을 것. `fn`이 `:Get()`을 안 하면 상류 State는 계속 `invalid`로 남는데, 만약 "이미 `invalid`면 전파를 멈춘다"는 규칙이 있으면 **이 Observer는 두 번째 변경부터 영원히 안 울림**. 실제로 2026-08-14 이전까지 위 "전파 모델 확정" 절에 그런 문장이 있었고, diff --git a/.claude/research/state-epoch-validation.md b/.claude/base/state-epoch-plan.md similarity index 82% rename from .claude/research/state-epoch-validation.md rename to .claude/base/state-epoch-plan.md index db33306..350067e 100644 --- a/.claude/research/state-epoch-validation.md +++ b/.claude/base/state-epoch-plan.md @@ -1,13 +1,20 @@ -# State 재계산 판정을 "소스 에포크" 비교로 바꾸는 안 (2026-08-21 신설) +# State 재계산/전파 판정 — 소스 에포크 비교 (2026-08-21 확정) -**상태**: research — **사용자 제안, 에이전트 분석 완료. 2026-08-21에 세부가 네 차례 갱신됐다(중복 통지도 접음 / 선언 안 된 의존성 조항 기각 / `rawInvalid` 기제와 `emit` 인자 정정 / 순회 처분을 `sourceCountMap`·`sourceEmitMap` 두 테이블로 확정). 이제 기제는 사실상 다 정해졌고 남은 건 **채택 여부 자체**다 — 여전히 미정.** -구현 전 QA 5라운드(`SS-2`/`SS-3`)에서 나왔고 사용자 스스로 *"더 생각해볼 -이야기라 백로깅이나 리서치에 들어가야할듯"*이라 함. 회신 원문은 -`qa-request/pre-implementation-qa-round5-response.md`. +**상태**: **확정.** 사용자 제안으로 시작해 같은 날 네 라운드에 걸쳐 다듬은 뒤 +**채택 확정**됨 — *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다. +채택하면 될것 같아요."* 구현은 **M3(Source/State)**. -**⚠️ `base/source-state-plan.md`가 여전히 정본이다** — 이 문서는 아무것도 -확정하지 않는다. 다만 **State 내부 표현을 바꾸는 제안이라 M3(=State 구현) -착수 전에 결론이 나야 한다.** +**⚠️ 이 문서는 `base/source-state-plan.md`의 "전파 모델 확정" 절을 대체하는 +게 아니라 그 절이 정한 push-invalidate/pull-recompute 위에 **판정 규칙**을 +얹는다.** 그 절이 원래 갖고 있던 두 서술은 이 채택으로 뒤집혔고(아래 §3), +역전 원문은 `archive/always-propagate-no-dedup-superseded.md`에 있다. + +**읽는 순서**: 규칙만 필요하면 **§2**만 보면 된다. §1은 왜 이걸 하는가(실재하는 +glitch), §3은 무엇이 고쳐지는가, §5는 세부 계약, §7은 구현 시 확인할 것. + +**히스토리**: 구현 전 QA 5라운드(`SS-2`/`SS-3`)에서 나왔고, 회신 원문은 +`qa-request/pre-implementation-qa-round5-response.md`, 네 라운드의 정정 경위는 +`qa-request/pre-implementation-qa-round5-followup.md`의 M·N절이 소스. ## 1. 사용자가 지목한 문제 (실재함) @@ -99,13 +106,15 @@ State `Get()`의 의미론을 바꾸는 결정이다.** - **고쳐진다 — 섞인 값.** 위 3단계에서 `D`가 `C:Get()`을 부르면, `C`도 자기 - `sourceList`에서 `A`의 count가 자기가 기록한 것보다 앞선 걸 보고 **신호가 + `sourceCountMap`에서 `A`의 count가 자기가 기록한 것보다 앞선 걸 보고 **신호가 아직 안 왔어도 스스로 재계산**한다. 그래서 `D`는 항상 `(B_new, C_new)`를 얻는다. **`Get()`이 "지금 이 순간의 일관된 값"을 반환한다는 보장이 처음으로 성립**한다. - **고쳐진다 — 중복 재계산.** 뒤늦게 `C` 쪽 신호가 도착해 `rawInvalid`가 켜져도, count 비교가 "이미 최신"이라 재계산이 안 일어난다. -- **⭐ [2026-08-21 후속 확정] 중복 *통지*도 같이 접는다.** 처음엔 "값만 +- **⭐ [2026-08-21 확정] 중복 *통지*도 같이 접는다 — `source-state-plan.md`가 + "접지 않는다"고 확정해뒀던 것의 역전이다**(역전 원문은 + `archive/always-propagate-no-dedup-superseded.md`). 처음엔 "값만 고쳐지고 통지는 두 번 그대로"로 정리했는데, **사용자 판정으로 통지도 같은 장치로 접기로 했다**: *"중복 통지는 단순히, count 목록을 순회해보고 이미 모든 소스가 최신이면 무시하는게 맞아보인다. 특히 emit 은 자신 소스를 @@ -202,10 +211,11 @@ State 사라진다. 다만 `base/relate-plan.md`가 경고하듯 **값이 키를 되참조하면 안 된다** — count는 숫자라 문제없다(테이블 identity 방식을 택하면 그 테이블이 source를 참조하지 않게 할 것). -6. **`Get`의 계약 문구.** 사용자 지적(*"Get 이 항상 최신 상태를 가져온다라는 - 말이 여기서 무력화되는 부분"*)의 방향은 사실 **반대**다 — 지금 모델이 - "최신"을 못 지키고 있었고, 이 제안이 그걸 지키게 만든다. 채택하면 - `base/source-state-plan.md`의 전파 모델 절을 그렇게 다시 써야 한다. +6. **[2026-08-21 반영 완료] `Get`의 계약 문구.** 사용자 지적(*"Get 이 항상 최신 + 상태를 가져온다라는 말이 여기서 무력화되는 부분"*)의 방향은 사실 **반대**였다 + — 옛 모델이 "최신"을 못 지키고 있었고 이 안이 그걸 지키게 만든다. + `base/source-state-plan.md`의 전파 모델 절은 **채택과 함께 그렇게 다시 + 썼다.** ## 6. 곁가지 — 폴링용 sugar @@ -216,20 +226,16 @@ Ref 로 변환해주는 등"*. 즉 "항상 최신 값을 필드로 들고 있 있다), `research/operator-sugar-plan.md`의 콤비네이터 계열에 더 가깝다 — 채택되면 그쪽으로 옮길 것. -## 7. 권고 +## 7. 구현 시 확인할 것 (채택 확정 후 남은 실무 항목) -- **채택 방향에 찬성.** "선제 최적화가 아니라 확정 동작으로의 승격"이라는 - 사용자 판단에 동의한다 — 이건 성능이 아니라 **정확성** 결정이고, State - 내부 구조를 정하는 M3보다 **뒤에 하면 되돌리는 비용이 크다**. -- **[2026-08-21] 에이전트가 붙였던 조건(선언 안 된 의존성을 UB로 명문화)은 - 사용자 기각으로 빠졌다** — §5의 1번 참고. -- **채택 시 같이 확정되는 것: 중복 통지도 접는다**(§3) — 그러면 다이아몬드에서 - 값도 통지도 한 번씩만 간다. **[2026-08-21 정정]** 여기 있던 "노드가 - `seen`/`computedAt` 두 카운트를 들어야 한다"는 추가 요구는 **철회**됐고, - **[2026-08-21 갱신]** 대신 확정된 것은 **테이블 둘**(`sourceCountMap` / - `sourceEmitMap`)이다 — 순회가 값만 앞당기고 통지는 상류 emit을 기다린다. - 그래서 `nil` emit 규약도, 게이트를 에포크 경계로 만드는 계약 반전도 필요 - 없다(§5의 3번). -- 실측은 `luau-test`에 스파이크 하나면 충분하다 — 위 다이아몬드를 그대로 짜서 - (a) 지금 모델에서 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면 - 사라지는지 대조. +- **역전된 두 서술은 `archive/always-propagate-no-dedup-superseded.md`에 있다** — + "emit은 자기 `invalid`와 무관하게 **항상** 전파된다"와 "quad가 접지 않는 것은 + 중복 *통지*뿐이다". 지금 계약은 **"`invalid`로는 절대 안 접고, 같은 소스의 같은 + 에포크가 두 번째로 도착했을 때만 접는다"**이다. 이 구분을 흐리면 2026-08-14에 + 폐기된 "영구 침묵" 버그로 되돌아간다(§3). +- **선언 안 된 의존성에 대한 UB 조항은 안 만든다** — §5의 1번(사용자 기각). +- **`luau-test` 스파이크 하나**: §1의 다이아몬드를 그대로 짜서 (a) 에포크 없이는 + 섞인 값이 실제로 관측되는지, (b) 에포크 비교를 넣으면 사라지는지 대조. +- **`sourceEmitMap`은 희소 `pending` 구현으로 시작해도 된다** — §2의 구현 메모. +- 곁가지였던 폴링 슈가는 §6 그대로 — 이 결정과 독립이고 `operator-sugar-plan.md` + 계열로 남는다. diff --git a/.claude/luau-test/README.md b/.claude/luau-test/README.md index 66b6141..d1815a9 100644 --- a/.claude/luau-test/README.md +++ b/.claude/luau-test/README.md @@ -11,7 +11,7 @@ | 폴더 | 뜻 | 누가 처리 | |---|---|---| | `review-required/` | **설계가 걸림 — 사람 결정 필요**(**[2026-08-13 13차 세션] 현재 비어 있음** — 마지막 한 건이던 `08`이 해소돼 `done/`으로 감) | ⭐ 사용자 | -| `rewrite-required/` | 스파이크가 낡음 — 코드가 깨졌거나(`13`/`15`), **설계가 바뀌어 옛 모델을 검증 중**(`04`/`19`, 2026-08-13 14차 세션 하강 diff / `10`, **[2026-08-14 5차 세션]** `canExecute` 1-인자 재정정 / `05`, **[2026-08-14 8차 세션]** "emit은 항상 전파" 정정) — **`16`은 [2026-08-15] 통과로 `done/`에 있음**(아래 참고) | 에이전트 | +| `rewrite-required/` | 스파이크가 낡음 — 코드가 깨졌거나(`13`/`15`), **설계가 바뀌어 옛 모델을 검증 중**(`04`/`19`, 2026-08-13 14차 세션 하강 diff / `10`, **[2026-08-14 5차 세션]** `canExecute` 1-인자 재정정 / `05`, **[2026-08-14 8차 세션]** "emit은 항상 전파" 정정 → 재작성 통과했다가 **[2026-08-21]** 소스 에포크 채택으로 다시 합류) — **`16`은 [2026-08-15] 통과로 `done/`에 있음**(아래 참고) | 에이전트 | | `not-run/` | 이 환경에서 못 돌림 — **[2026-08-14 5차 세션] 스파이크는 0건**(`10`이 `rewrite-required/`로 감), GC 헬퍼만 남음 | 사용자 or MCP 연결 후 | | `done/` | 통과 or 판정 끝, 더 할 일 없음 | — | @@ -71,7 +71,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해 | `02-none-sentinel-vs-nil-holes.luau` | **[2026-08-09 커밋 f198fd9 반영해 전면 재작성]** 순서가 중요한 배열(PreRef pre-pass, sourceList)은 `None` 소진이 맞고, 순서가 안 중요하고 재사용이 필요한 배열(Ref 콜백/대기자)은 `nil`+슬롯 재사용이 맞다는 최종 구분 + `None`을 잘못 쓰면 배열이 무한정 자라는 버그의 정량적 재현 | `ref-plan.md` "왜 None이 아니라 nil인가"(2026-08-09 열한 번째 세션 최종 정정), ROADMAP M0-4 | | `03-recursive-store-bind-dispatch.luau` | `process`/`retract` 재귀 재-dispatch 기본 모델, 우선순위 스캔 | `dispatch-core-plan.md` "확정된 디스패치 모델", ROADMAP M0-3 | | `04-dispatch-chain-retractFrom.luau` | **[⚠️ 2026-08-13 열네 번째 세션: 하강 diff 확정으로 낡음 → `rewrite-required/`]** 아래는 옛 모델 기준 설명 — **[2026-08-13 감사에서 전면 재작성 + 파일명 변경]** 인덱스 기반 `chains`/`Dispatch.retractFrom`이 다단 재귀 위임에서 정확한지 — 3단 체인이 인덱스 1/2/3으로 안 겹치고 쌓이는지(= `State>` **정상 동작**, UB 아님), 안/바깥 store 재발행 시 깊은 인덱스부터 정리되는지, hint가 target 인덱스에만 가는지 + **음성 대조군**: `chains:SetStrong`을 `handler.process` 뒤에 두면 최초 마운트에서 하위 retractor가 유실되는 버그 재현. 옛 버전은 핸들러 identity 기반 추적과 "중복 push 즉시 error" 가드를 검증했는데 그 가드는 다섯 번째 세션 재설계로 **없어져서** 설계와 정반대를 테스트하고 있었음 | `dispatch-core-plan.md` "Dispatch 체인"(2026-08-13 다섯 번째 세션 재설계) + 2026-08-13 감사 | -| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지. **[2026-08-19 재작성 완료 → `done/`]** 현행 모델("emit은 자기 invalid 상태와 무관하게 항상 전파, 중복 재계산은 `:Get()` 시점 캐시로만 막힘")로 다시 짜서 통과 — 핵심 회귀 방지 장치는 `:Get()`을 안 부르는 Observer가 다이아몬드에서 source 변경마다 경로 수(2)만큼 계속 우는지(옛 모델이면 두 번째부터 침묵) | ROADMAP M0-1 | +| `05-store-state-diamond-propagation.luau` | push-invalidate/pull-recompute가 다이아몬드 의존성에서 중복 재계산 없이 동작하는지. **[2026-08-19 재작성 → 2026-08-21 다시 `rewrite-required/`]** 2026-08-19엔 당시 모델("emit은 항상 전파, 중복 재계산은 `:Get()` 시점 캐시로만 막힘")로 짜서 통과했으나, **소스 에포크 비교 채택(`base/state-epoch-plan.md`)으로 다이아몬드 두 번째 통지가 접히게 되어** 핵심 assert가 정반대가 됨 — 이제 Observer는 변경당 **1회**만 울어야 한다. 상태의 소스는 `STATUS.md` | ROADMAP M0-1 | | `06-component-boundary-nil-hole-props.luau` | `props.Modifier or None` 관용구가 컴포넌트 경계 nil-hole을 막는지 + `Params` 타입 체크 | `component-composition-plan.md` "필수 관용구", ROADMAP M0-5 | | `07-relate-weak-table-gc.luau` | `Relate`의 lazy 서브테이블 생성 + weak-key GC가 실제로 동작하는지 | `relate-plan.md` "M2 착수 시 실측 확인" **[2026-08-13 보강]** 4번 섹션 신설 — `_countEntries()`(테스트 전용) + weak-value canary로 **"inst가 죽으면 중첩 StrongMap 안의 payload까지 연쇄 GC되는가"를 직접 검증**(원래는 sanity check만 하고 헤더의 핵심 주장은 미검증이었음). 파일이 스스로 적어둔 "weak table 엔트리를 셀 표준 API가 없다"는 전제도 틀렸음 — outer가 `__mode="k"`라 GC 후 `pairs`에서 사라짐 | | `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source`가 `State`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `base/source-state-plan.md` "Source가 State를 만족함", ROADMAP M0-2 | diff --git a/.claude/luau-test/STATUS.md b/.claude/luau-test/STATUS.md index 41c0e9f..79e1f6d 100644 --- a/.claude/luau-test/STATUS.md +++ b/.claude/luau-test/STATUS.md @@ -82,13 +82,14 @@ `10`은 **Studio 전용이라 재작성해도 이 환경에서는 못 돌린다** — 재작성 후 다시 `not-run/`으로 내려가 사용자/MCP를 기다리는 자리다. -**[2026-08-21] `01`이 합류** — 같은 "설계가 바뀐" 유형이다. 통과 상태로 -`done/`에 두면 이제는 구현이 안 하는 두 루프 순회를 "검증됨"으로 오독하게 -된다. +**[2026-08-21] `01`과 `05`가 합류** — 둘 다 같은 "설계가 바뀐" 유형이다. +통과 상태로 `done/`에 두면 `01`은 구현이 안 하는 두 루프 순회를, `05`는 +**이제 접히는 중복 통지가 안 접힌다는 것**을 "검증됨"으로 오독하게 된다. | 파일 | 상태 | 무엇을 고쳐야 하나 | |---|---|---| | `01-two-pass-array-hash-order.luau` | 옛 형태 기준으로는 ✅ 통과였음 | 숫자 `for` + 일반화 `for` **두 루프**로 짜여 있는데, 구현은 **단일 일반화 `for`**로 정정됨(`base/dispatch-core-plan.md`의 "props 순회 순서" 절, QA 4라운드 `F-4-1`) — Luau의 일반화 `for`가 배열 파트를 먼저 다 돌고 해시 파트로 넘어간다는 것 자체를 **한 루프로** 검증하도록 다시 쓸 것. **검증 대상(순서 계약)은 그대로**라 결론이 바뀌는 건 아님 | +| `05-store-state-diamond-propagation.luau` | 2026-08-19 재작성분은 그 시점 모델 기준 ✅ 통과였음 | **[2026-08-21] 모델이 또 바뀌었다** — 소스 에포크 비교 채택(`base/state-epoch-plan.md`)으로 다이아몬드에서 **두 번째 통지가 접힌다**. 그래서 이 스파이크의 핵심 assert("`:Get()`을 안 부르는 Observer가 변경당 경로 수(2)만큼 운다")가 **정반대**가 됐다 — 이제 **변경당 1회**여야 한다. **살릴 것**: `invalid` 기반 dedup이면 두 번째 변경부터 침묵하는 것을 잡는 음성 대조군(그 금지는 지금도 유효). **새로 넣을 것**: DFS 도중 `Get()`이 섞인 값을 캐시하던 glitch가 에포크로 사라지는지(그 문서 §1의 시나리오) | | `04-dispatch-chain-retractFrom.luau` | 옛 모델 기준으로는 ✅ 통과였음 | (1) `chains` 슬롯이 `{handler, retractor}`가 되고 `Dispatch.process`가 핸들러를 먼저 비교하는 **하강 diff**로 재작성, (2) `retractFrom`은 **3-인자**(힌트 인자 없음), (3) "힌트가 target 인덱스에만 간다"를 검증하던 부분은 **정반대**로 뒤집힘 — 이제 각 레벨이 자기 값을 받는지를 검증해야 함. **살릴 것**: `chains:SetStrong` 순서 음성 대조군(그 버그는 새 모델에서도 그대로 유효) | | `19-ownership-refcount-relate-patterns.luau` | A/C ✅ 유효, **B 섹션이 낡음** | B가 검증하던 "공개 `AttributeKey(name)` + 인덱스 1 점유 체크"가 폐기됨 — **그룹 전용 키 + `AttributeKeyHandler`의 이름 claim**으로 재작성하고, 음성 대조군도 "두 그룹이 같은 이름 → 즉시 error", "그룹↔직접 쓰기 → 즉시 error"로 바꿀 것(0-Z 확정 내용). A/C는 손댈 것 없음 | | `15-type-compute-trailing-deps-typepack.luau` | **파싱 실패**(SyntaxError) | 음성 대조군의 타입 표기가 `TypeError`가 아니라 `SyntaxError`로 걸려 **파일 전체가 아무것도 검증 못 함** — 대조군을 별도 파일/블록으로 격리 | @@ -115,7 +116,6 @@ |---|---| | `02-none-sentinel-vs-nil-holes` | `nil` 소진 시 `#t` 50→49로 무너짐 / `None`은 항상 50. 반대로 Ref 콜백 배열은 `None` 쓰면 죽은 슬롯 1000개 잔존 — **두 배열의 규칙이 서로 반대여야 함**이 정량 확인 | | `03-recursive-store-bind-dispatch` | StoreBind 재귀 재-dispatch, `None`→`nil` 흐름, 무한재귀 없이 종료 | -| `05-store-state-diamond-propagation` | **[2026-08-19 재작성]** emit은 자기 invalid 상태와 무관하게 항상 전파(다이아몬드 두 경로 모두 끝까지 도달), 재계산은 `:Get()` 시점 캐시로 1회만, `:Get()`을 안 부르는 Observer는 source 변경마다 경로 수만큼(2) 계속 발화 — 옛(역전된) 모델이면 2번째 변경부터 침묵해야 하는데 안 그럼을 확인 | | `06-component-boundary-nil-hole-props` | `or None` 없으면 앞쪽 nil-hole로 슬롯 소실, 관용구 쓰면 항상 보존 | | `07-relate-weak-table-gc` | **연쇄 GC 확정**(아래 별도 절) — GC-native 아키텍처의 핵심 전제 | | `11-modifier-illegal-value-error` | Modifier 필드/Source에 핸들러 계층 값 넣으면 즉시 error — 16개 케이스 전원 | diff --git a/.claude/luau-test/done/05-store-state-diamond-propagation.luau b/.claude/luau-test/rewrite-required/05-store-state-diamond-propagation.luau similarity index 100% rename from .claude/luau-test/done/05-store-state-diamond-propagation.luau rename to .claude/luau-test/rewrite-required/05-store-state-diamond-propagation.luau diff --git a/.claude/qa-request/pre-implementation-qa-round5-followup.md b/.claude/qa-request/pre-implementation-qa-round5-followup.md index 2d0b5f2..33adcbe 100644 --- a/.claude/qa-request/pre-implementation-qa-round5-followup.md +++ b/.claude/qa-request/pre-implementation-qa-round5-followup.md @@ -952,3 +952,50 @@ state 의 전파를 손대는 작업이라 with 처럼 다른 노드가 나는 그대로 유효하고(팩토리가 내부에서 `:Gate`를 부름), `Blocker` 배선은 이미 확정된 `state:Block(blocker)` 메소드로 자동 해소되며, `__call`은 안 쓴다. `research/gate-primitive.md`의 2번이 해소로 갱신됨. + +--- + +# O절 — `Gate` 표면 확정 + State 에포크 **채택** (2026-08-21, 5라운드 종결) + +두 건이 같은 대화에서 닫히며 **M2 착수를 막던 설계 항목이 전부 사라졌다.** +두 문서 모두 `research/` → **`base/`**로 승격됐다. + +## O-1. `Gate` — `state:Gate(setup)` 메소드 + `GateNode` + +**사용자 확정**: *"Gate 는 따로 프리미티브 없이 `state:Gate( (emit) -> ()->() )` +처럼 선언되고 마치 Compute 처럼 GateNode(ComputeNode 처럼) 생성된다 그리고 +Blocker 는 해당 내부 배선을 따른다 ← 동의합니다 해당 방법대로 확정하면 됩니다."* + +처음 방향(*"공용 `Gate` 프리미티브를 꺼내고 `Blocker`가 컴포지션한다"*)이 +뒤집힌 것 — *"그럴 필요가 없네요. `:Gate` 는 또 Apply 에서 쓸만한 표면을 +주기도 하구요."* 따라온 결론: + +- 탑레벨 `Gate(...)` 생성자는 **안 만든다.** 이름 문제(`Gater`?)도 같이 소멸 — + 메소드 자리에서 `Gate`는 `:With`/`:Compute`와 나란히 자연스럽다. +- `Blocker`는 그 위의 별개 프리미티브. `state:Block(blocker)`가 내부에서 + `self:Gate(policy)`를 부른다(`base/blocker-plan.md`에 신설 절). +- `Debounce`/`Throttle`의 `state:Apply(...)` 관용구는 그대로 — 팩토리가 + 내부에서 `:Gate`를 부른다(`base/debounce-throttle-plan.md`의 권고 절이 + "실현됨"으로 갱신). +- `Get()`엔 영향 없음(통지만 막음)도 확정 — **`state-epoch-plan.md`가 이 + 계약에 의존한다.** + +## O-2. State 에포크 — **채택 확정** + +*"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다. 채택하면 될것 +같아요."* 구현은 M3. 규칙 전량은 `base/state-epoch-plan.md`. + +**같이 역전된 것**(원문 → `archive/always-propagate-no-dedup-superseded.md`): +`source-state-plan.md`가 확정해뒀던 두 서술 — emit은 자기 `invalid`와 +무관하게 **항상** 전파된다, 그리고 quad가 접지 않는 것은 중복 *통지*뿐이다. 지금 계약은 **"`invalid`로는 절대 +안 접고, 같은 소스의 같은 에포크가 두 번째로 도착했을 때만 접는다."** +⚠️ 2026-08-14의 역전(`invalid` 기반 dedup 금지)을 되돌린 게 **아니다**. + +**반영 파일**: `base/state-epoch-plan.md`·`base/gate-plan.md`(승격), +`base/source-state-plan.md`(전파 모델·다이아몬드 절 재작성), +`base/blocker-plan.md`, `base/debounce-throttle-plan.md`, +`base/architecture.md`, `reference/comparison-fusion-vide.md`, +`archive/always-propagate-no-dedup-superseded.md`(신설), `ROADMAP.md`(M0 각주· +M2 각주·M3 체크박스), `README.md`, `question.md`, `todos.md`, +`luau-test/`(스파이크 `05`가 `rewrite-required/`로 되돌아감 — 다이아몬드 +Observer가 이제 변경당 **1회**만 울어야 하므로 핵심 assert가 정반대가 됨). diff --git a/.claude/question.md b/.claude/question.md index 102e8b5..97e69dd 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -65,7 +65,7 @@ 그대로**(`gate`는 이미 행위자가 아니라 **장치**를 가리키는 명사라 `-er`가 불필요 — `Source`/`Ref`/`Slot`/`Tween`도 같은 계열), 대안 후보는 `Valve`/`Relay`. **설계 자체가 다음 세션으로 미뤄졌으므로 이름도 그때 같이** - — 3번 절의 `Gate` 항목과 `research/gate-primitive.md`가 소스. + — 3번 절의 `Gate` 항목과 `base/gate-plan.md`가 소스. - **`Slot`(2순위)**: Vue의 "slot"(콘텐츠 주입 지점)과 이름은 같지만 의미가 다름(quad의 Slot은 자식 배열 재조정 프리미티브) — Vue 배경 있는 사람이 헷갈릴 수 있음. @@ -205,8 +205,14 @@ 빈도가 다르다.** 해법은 있다 — reconcile이 `pos`처럼 **절대 offset도 러닝 누적**으로 들고 다니면 O(n)(그게 `mountSlotTree`가 이미 하는 방식). 그렇게 할지, 아니면 실측 전엔 그냥 둘지 판단 필요. -- **⭐ [신설, 2026-08-21 구현 전 QA 5라운드] 공용 `Gate` 노드의 이름과 표면 — - M2 착수 전 필요.** 위 항목의 결정("게이팅 먼저")에 따라 base에 만들 것이 +- **[해소, 2026-08-21] 공용 게이트 노드의 이름과 표면 — `state:Gate(setup)` + 메소드 + `GateNode`로 확정.** 탑레벨 프리미티브는 안 만들고, `Blocker`는 + `state:Block(blocker)` 안에서 그 배선을 쓴다(사용자: *"Gate 는 따로 + 프리미티브 없이 state:Gate( (emit) -> ()->() ) 처럼 선언되고 마치 Compute + 처럼 GateNode(ComputeNode 처럼) 생성된다"*). `Get()`엔 영향 없음(통지만 + 막음)까지 확정. **남은 것은 사용자 판단이 아니라 구현 시 정할 것들** + (생명주기·재진입 계약, M2에 `Blocker`까지 넣을지) — `base/gate-plan.md`가 + 소스. 아래는 열려 있던 시점의 서술: 위 항목의 결정("게이팅 먼저")에 따라 base에 만들 것이 `Blocker`가 아니라 **상류 emit을 가로채 정책이 통과 여부를 정하는 공용 게이트 노드**로 확정됐다(`Blocker`/`Debounce`/`Throttle`이 그 위의 정책). 사용자 스케치는 `Gate(function(emit) return function() ... end end)` 2단 구조이고, @@ -219,9 +225,11 @@ `:With`류 State 메소드**(*"state 의 전파를 손대는 작업이라 with 처럼 다른 노드가 나는게 맞음"*)이고, 그러면 `Blocker`는 이미 확정된 `state:Block(blocker)` 메소드가 내부에서 그걸 부르면 되므로 배선 문제 자체가 - 없어진다. 상세는 `research/gate-primitive.md`. -- **⭐ [신설, 2026-08-21 구현 전 QA 5라운드] State 재계산 판정을 "소스 에포크 - 비교"로 바꿀지 — M3 착수 전 필요.** 사용자 제안: 각 State가 자기 상류 루트 + 없어진다. 상세는 `base/gate-plan.md`. +- **[해소, 2026-08-21] State 재계산/전파 판정을 "소스 에포크 비교"로 바꿀지 — + 채택 확정**(사용자: *"gate 와 epoch 가 제가 만족할만한 정도로 올라왔습니다. + 채택하면 될것 같아요."*). 규칙 전량은 `base/state-epoch-plan.md`, 구현은 M3. + 아래는 열려 있던 시점의 서술: 사용자 제안: 각 State가 자기 상류 루트 `Source`들의 카운트를 들고 있다가 `Get()` 때 비교해 재계산 여부를 정한다. 동기는 성능이 아니라 **정확성** — DFS 전파 도중 Observer가 `Get()`을 부르면 아직 신호를 못 받은 다른 가지의 옛 캐시가 섞여 들어가는 glitch가 지금 모델에 @@ -237,7 +245,7 @@ 기다립니다"*). 그래서 통지가 죽지도 게이트를 새지도 않아 `source = nil` 규약도 필요 없어졌다. **남은 판단은 채택 여부 자체 하나**이고, State 내부 표현을 바꾸는 결정이라 M3 뒤로 미루면 되돌리는 비용이 크다. 상세는 - `research/state-epoch-validation.md`. + `base/state-epoch-plan.md`. - **[해소, 2026-08-21 구현 전 QA 5라운드 `C-4`] `Dispatch.setLength`의 Observer 앵커 — 물리 target으로 확정(4라운드 `D-56` 역전).** `setLength`가 `(ownerKey, i, len, anchor)`로 4번째 인자를 받아 **부기 키와 생명주기 앵커를 diff --git a/.claude/reference/comparison-fusion-vide.md b/.claude/reference/comparison-fusion-vide.md index aa116bb..8ce5252 100644 --- a/.claude/reference/comparison-fusion-vide.md +++ b/.claude/reference/comparison-fusion-vide.md @@ -44,8 +44,12 @@ Store/Slot/Tween/bind-dispatch 설계 결정에 근거로 인용될 때만 열 **[2026-08-14 보강]** quad가 이걸 피하는 방식은 "전파를 중간에 끊는 것"이 아니라 **애초에 push 시점에 계산을 안 하는 것**(pull-recompute + 노드별 캐시) — 신호는 두 경로로 두 번 도착해도 계산은 `:Get()` 때 한 번뿐. - 즉 quad도 중복 *통지*는 접지 않고 중복 *재평가*만 안 일어남 + 즉 quad도 중복 *재평가*는 안 일어남 (`base/source-state-plan.md`의 "다이아몬드 의존성은 무엇이 푸는가" 절). + **[2026-08-21 갱신]** 여기 있던 "중복 *통지*는 접지 않는다"는 뒤집혔다 — + 소스 에포크 비교를 채택해 **두 번째 통지도 접힌다** + (`base/state-epoch-plan.md`, 역전 원문은 + `archive/always-propagate-no-dedup-superseded.md`). - **정리 모델**: 의존성 엣지(`parents`)와 구조적 소유(`owner`/`owned`)를 같은 `Node`에서 두 개의 별도 관계로 분리 — CHANGELOG 0.2.0에서 "destroy가 더 이상 reactive dependent까지 타고 내려가지 않고 owned만" 으로 명시적으로 고침(초기 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 6f0b630..db81441 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 @@ -260,3 +260,22 @@ dedup, 상류의 진짜 emit을 기다림)을 따로 둔다. 순회는 emit을 태워도 누출이 남고, `nil` emit은 하류마다 전체 순회를 강제해 연쇄한다). M절에서 철회했던 두-카운트 분리가 **다른 근거로 되살아난** 셈이다. 이제 이 안은 기제가 다 정해졌고 **남은 건 채택 여부 자체**다. + +## 11. 종결 — `Gate` 표면 확정 + 에포크 채택, 두 문서 `base/` 승격 (같은 날) + +사용자가 `Gate`를 **탑레벨 프리미티브 없이 `state:Gate(setup)` 메소드 + +`GateNode`**로 확정하고(*"Blocker 는 해당 내부 배선을 따른다"*), 이어서 +State 에포크도 **채택**했다(*"gate 와 epoch 가 제가 만족할만한 정도로 +올라왔습니다"*). `research/gate-primitive.md` → `base/gate-plan.md`, +`research/state-epoch-validation.md` → `base/state-epoch-plan.md`로 승격. + +에포크 채택으로 `source-state-plan.md`의 두 확정 서술("항상 전파" / "중복 +통지는 안 접음")이 역전돼 `archive/always-propagate-no-dedup-superseded.md`로 +옮겼다. **2026-08-14의 `invalid` 기반 dedup 금지를 되돌린 게 아니라는 것**을 +역전 문서·`source-state-plan.md`·`README.md` 세 곳에 모두 못박았다 — 이 +구분이 흐려지면 "영구 침묵" 버그로 되돌아간다. + +부수로 스파이크 `05-store-state-diamond-propagation`이 다시 +`rewrite-required/`로 갔다(다이아몬드 Observer가 이제 변경당 1회만 울어야 +해서 핵심 assert가 정반대). 처리 전량은 +`qa-request/pre-implementation-qa-round5-followup.md`의 O절. diff --git a/.claude/todos.md b/.claude/todos.md index cd03214..78a2487 100644 --- a/.claude/todos.md +++ b/.claude/todos.md @@ -6,9 +6,14 @@ 00. **⭐⭐ [2026-08-18 신설] 구현 전 QA — [2026-08-21] 1~5라운드 전부 `base/` - 반영 완료. 열린 항목은 **`Gate`(공용 게이트 노드) 하나뿐**이고, 그게 지금 - **M2 착수를 막는 유일한 항목**이다**(사용자 지시로 설계는 다음 세션 — - 재료는 `research/gate-primitive.md`). + 반영 완료. ⭐ 같은 날 마지막에 `Gate`와 State 에포크까지 확정되면서 + **M2 착수를 막는 설계 항목은 더 이상 없다.**** `Gate`는 + `state:Gate(setup)` + `GateNode`로(`base/gate-plan.md`), State의 + 재계산/전파 판정은 **소스 에포크 비교** 채택으로(`base/state-epoch-plan.md`, + 구현은 M3) 닫혔다. 두 문서 모두 `research/`가 아니라 **`base/`**에 있다. + 남은 것은 판단이 아니라 구현 시 정할 것들 — `Gate`의 생명주기·재진입 + 계약, M2에 `Blocker`까지 넣을지, 그리고 스파이크 `05` 재작성 + (`luau-test/STATUS.md`). **4라운드 — [2026-08-21] 종결.** 문항지는 `.claude/qa-request/pre-implementation-qa-round4.md`, 사용자 회신 원문은 @@ -39,10 +44,10 @@ `setLength` 앵커를 물리 target으로 되돌리기, `Gate` 이름·표면, `Effect` 다중 의존성) — **[2026-08-21 전량 처리 완료]**, 소스는 그 파일의 **마지막 절**(A~L). 대화 마지막 라운드에서 **`native*` 물리 조작 계층**이 확정되며 - 4라운드 `C-7`("부기가 물리보다 먼저")이 역전됐다. 그중 **`Gate`(공용 게이트 노드) 설계만 사용자 지시로 다음 - 세션으로 미뤄졌다**(*"고칠것이 많으므로 Gate 는 다음 세션에 다루겠음 … - 지금 세션 상 지식만 이전될 수 있게 두세요"*) — 재료는 - `research/gate-primitive.md`에 모아뒀고 **M2 착수를 막는 유일한 항목**이다. + 4라운드 `C-7`("부기가 물리보다 먼저")이 역전됐다. `Gate` 설계는 한때 + 다음 세션으로 미뤄졌으나(*"고칠것이 많으므로 Gate 는 다음 세션에 + 다루겠음"*) **[2026-08-21 같은 날 확정]** 사용자가 이어서 표면을 정해 + `base/gate-plan.md`가 `research/`를 떠났다 — 위 00번 머리말이 소스. 아래는 4라운드 회신 전 서술: **(원 서술) 4라운드 문항지 작성 경위.** 사용자 요청("모든 확정 부분에 있어서 예가 되어야하는 질문들을 @@ -233,8 +238,9 @@ 기능 갭**에서 시작됨(그 문서 13절). 다만 제어 핸들 설계까지 닫히고 나니 실제로 quad-base에 새 코어 메커니즘을 추가하지 않는 **순수 슈가**로 확인돼(같은 절), 위 항목들과 우선순위는 다시 같아짐 — M0/M3를 막지 - 않고, **M3에서 `Blocker`를 구현할 때 게이티드 노드를 공용 `Gate`로 - 빼두는 것만은 그 시점에 해야 함**(따로 하면 같은 설계를 두 번 함). + 않고, **그 게이티드 노드는 [2026-08-21] `state:Gate`로 확정돼 M2에서 + 만들어진다**(`base/gate-plan.md`) — `Debounce`/`Throttle`은 그 위의 + 정책으로 얹으면 되고, 같은 설계를 두 번 할 일은 없어졌다. 주입 op 2개(`setTimeout`/`clearTimeout`)가 백엔드 팩토리 표면에 추가될 예정이라는 것도 M1 설계 시 인지. 남은 열린 질문 없음(구 `question.md` 3번, 전량 해소로 항목 자체가 빠짐). diff --git a/ROADMAP.md b/ROADMAP.md index 86608f2..3076c79 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -40,13 +40,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 - [x] Store/State push-invalidate → pull-recompute propagation을 실제로 짜보기(다이아몬드 의존성 케이스 포함 — **[2026-08-14 정정]** 확인할 것은 "이미 invalid면 전파 중단되는지"가 **아니라** 그 반대: - **emit은 자기 invalid 상태와 무관하게 항상 전파되고**, 중복 재계산은 - `:Get()` 시점 캐시로만 막히는지. 특히 `:Get()`을 안 부르는 + **emit은 자기 invalid 상태와 무관하게 전파되고**, 중복 재계산은 + `:Get()` 시점 캐시로만 막히는지(**[2026-08-21]** 그 뒤 "항상"에서 + "같은 에포크의 두 번째만 접힘"으로 좁혀졌다 — 아래 참고). 특히 `:Get()`을 안 부르는 `Observer`가 매 변경마다 계속 울리는지 — 옛 모델에선 두 번째부터 침묵했음(`archive/invalidate-dedup-propagation-reversed.md`). 스파이크 `05-store-state-diamond-propagation.luau`는 **[2026-08-19 - 재작성 완료, `done/`]** 현행 모델("emit은 항상 전파 + `:Get()` - 시점 캐시로만 dedup")로 재검증 통과) + 재작성 완료]** 그 모델("emit은 항상 전파 + `:Get()` 시점 캐시로만 + dedup")로 재검증 통과 — **[2026-08-21] 그 모델이 다시 바뀌어 + `rewrite-required/`로 되돌아갔다**(소스 에포크 채택으로 다이아몬드 + Observer가 이제 변경당 **1회**만 울어야 함, `base/state-epoch-plan.md`)) - [x] Source가 State를 구조적으로 만족하는 제네릭 타입(`:Compute(self: Source, ...) -> State`류, self 타이핑 + State 참조 혼합)이 Luau 솔버에서 안전하게 추론되는지 확인(2026-08-06 세 번째 세션, @@ -245,8 +248,10 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 노드**로 바뀌었다(같은 라운드 `DT-4` — 시간 기반 게이트는 공개 `Blocker` API 위에 못 얹히므로, emit을 가로채는 노드를 한 겹 일반화해 `Blocker`/`Debounce`/`Throttle`이 그 위의 정책이 되게 - 한다). **표면/이름은 아직 미정** — `research/gate-primitive.md`가 - 소스이고 `question.md` 3번에 열린 항목으로 올라가 있다. 최소한 + 한다). **[2026-08-21 확정] 그 노드의 표면은 `state:Gate(setup)` + 메소드이고 노드 타입 이름은 `GateNode`**(`ComputeNode`와 같은 층위) + — 탑레벨 `Gate(...)` 프리미티브는 안 만든다. 상세와 남은 항목 + (생명주기·재진입 계약, M2 범위)은 `base/gate-plan.md`. 최소한 배치 등록이 실제로 쓰는 `On`/`IsOn`/`OffWithoutEmit`이 도는 형태까지는 M2에 필요하다. 경위는 `qa-request/pre-implementation-qa-round3.md`의 "ROADMAP.md 마일스톤 @@ -342,14 +347,16 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 되면 동종 타입 dep 1개로 한정) - [ ] **[2026-08-21 5라운드 — 순서 주의]** 아래 `Blocker.luau`가 올라서는 **공용 게이트 노드는 M2에서 이미 만들어져 있다**("게이팅 먼저" 결정, - 위 M2 각주 + `research/gate-primitive.md`). M3에서 `Blocker`를 짤 때는 + 위 M2 각주 + `base/gate-plan.md`). M3에서 `Blocker`를 짤 때는 그 노드를 **다시 만들지 말고 그 위의 정책으로** 얹을 것. -- [ ] **[2026-08-21 5라운드 — 착수 전 확인]** State 재계산 판정을 "소스 - 에포크 비교"로 바꿀지가 **미정인 채 열려 있다**(`research/state-epoch-validation.md`, - `question.md` 3번). 채택하면 **State 내부 표현이 바뀌므로**(노드가 - 루트 Source별 count 테이블 둘을 들고, `Get`이 상류 에포크를 검증) - 아래 `Source.luau`/`State.luau`를 짜기 **전에** 결론이 나야 한다 — - 그 문서 자신이 "M3 뒤로 미루면 되돌리는 비용이 크다"고 경고한다. +- [ ] **[2026-08-21 5라운드 — 채택 확정]** State의 재계산/전파 판정은 + **소스 에포크 비교**다(`base/state-epoch-plan.md`) — `invalid` 플래그가 + 아니다. 아래 `Source.luau`/`State.luau`가 이걸 전제로 짜여야 한다: + 노드가 `sourceCountMap`(값 유효성)/`sourceEmitMap`(전파 dedup) 두 + 테이블을 들고, emit은 발행 source만 싣고, 순회는 `rawInvalid == false` + 일 때만 돌며 **값만 앞당기고 통지는 상류 emit을 기다린다**. 다이아몬드 + 중복 통지가 접히므로 스파이크 `05`도 그에 맞춰 재작성해야 한다 + (`luau-test/STATUS.md`). - [ ] `Blocker.luau`(`base/blocker-plan.md` 참고 — 여러 Source를 한꺼번에 바꿔도 파생값 재계산/재대입이 한 번만 되게 하는 primitive, State와 밀접히 연관돼 있어 같은 마일스톤에서 개발) @@ -1021,8 +1028,8 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검 그 공용 `Gate` 추출은 M3가 아니라 M2로 앞당겨졌다** — "게이팅 먼저" 결정(위 M2 각주)으로 `Dispatch.drive`의 배치 등록이 쓰는 게이팅부터 만들기로 했고, 그때 `Blocker`/`Debounce`/`Throttle`이 공유할 노드를 - 같이 빼둔다(따로 하면 같은 설계를 두 번 함). 표면/이름은 아직 미정 — - `research/gate-primitive.md`. + 같이 빼둔다(따로 하면 같은 설계를 두 번 함). **[2026-08-21] 표면 확정 — + `state:Gate(setup)` 메소드**, `base/gate-plan.md`. 프리미티브 자체는 그 위에 나중에 얹으면 되고 M0/M3를 막지 않음. 주입 op 2개(`setTimeout(func, delay) -> Timeout` / `clearTimeout`, Roblox는 `task.delay`/`task.cancel`로 배선 — **인자 순서가 반대라