diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 998a51e..5519397 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -204,7 +204,8 @@ push-invalidate(신호만)/ pull-recompute(`Get()` 시점) — Fusion식 eager 노드 없이도 다이아몬드 의존성 중복 재계산 문제가 풀림. State는 쓰기 대상이 아니고, 값을 쓰는 경로는 `source:Set(value)`(Source가 State보다 넓은 인터페이스를 가짐 — -`.value`/`:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가). 값 하나만 +`:Get()`/`:With`/`:Compute` 위에 `:Set`/`:Emit` 추가; [정정, 2026-08-07] +읽기는 `:Get()` 하나로 통일 — `.value` 표기는 Ref 전용으로 좁혀짐). 값 하나만 다룰 땐 Store와 별개인 가벼운 `Source` 프리미티브를 독립적으로도 씀. `store.key` dot-access를 타입 추론 1급 경로로 삼는 것도 3차 라운드에서 정식 확정됨 — **더 이상 열린 질문 아님**, 남은 건 정확한 API 이름뿐. diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index 506850b..9103d6b 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -404,7 +404,7 @@ Modifier와는 다른 부류). ```lua local observer = state:Observer(function() - state.value + state:Get() end) Frame { @@ -419,7 +419,7 @@ retract/Destroy되면 자동으로 정리됨. 무효화 신호 하나로 좁혀짐 — 값을 안 실어보내므로 저렴함" 원칙(아래 "Store/State/Source 온톨로지" 절)이 그대로 적용됨: `fn`은 "뭔가 바뀌었으니 다시 확인하라"는 신호만 받고 새 값 자체는 안 받음 — - 위 예시처럼 `fn` 본문에서 `state.value`/`Get()`을 명시적으로 다시 + 위 예시처럼 `fn` 본문에서 `state:Get()`을 명시적으로 다시 읽어야 함. 자동으로 안 해주는 이유: 재계산이 진짜 필요한지가 다른 `:With`한 값에 따라 갈리는 경우가 있어서(위 "포지셔널 인자 지양" 절의 `noprint` 예시처럼 계산 자체를 통째로 생략하고 싶을 수 있음) — `Get()` @@ -532,7 +532,7 @@ read ...`처럼, State끼리 자유롭게 합성/파이핑 가능한 것이 최 아래 "State는 쓰기 대상이 아님" 절 이후 내용 및 `base/store-semantics.md`의 "Source가 State를 만족함" 절 참고): - **Source** — 실제 값이 존재하고 변경될 수 있는 단일 지점(v1의 "값의 근원"). - **구조적으로 State를 만족(단방향 호환)** — `.value`/`:Get()`/`:With`/`:Compute` + **구조적으로 State를 만족(단방향 호환)** — `:Get()`/`:With`/`:Compute` 전부 지원 위에 `:Set(value)`/`:Emit()` 추가. - **Store** — Source들의 이름 붙은 모음, 그 이상 아님. `store.a`처럼 키로 접근하면 **이미 만들어진 Source가 있으면 그대로 반환, 없으면 그 자리에서 @@ -557,7 +557,7 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦** - 신호를 받은 State는 자기 `invalid` 플래그만 세우고, 이미 `invalid`였다면 그 아래로 더 전파하지 않는다 — 다이아몬드 의존성에서 중복 워크를 막는 장치(Vide가 저자 스스로 `todo.md`에 미해결로 남긴 문제의 해결책). -- 실제 재계산은 `Get()`(또는 `.value` 인덱싱)이 호출되는 시점에만 일어남 — +- 실제 재계산은 `:Get()`이 호출되는 시점에만 일어남 — "필요할 때 계산" 원칙(사용자 확정). Fusion의 `timeliness="eager"` 노드/ 생성순 정렬 장치는 만들지 않음 — quad엔 그런 다단계 즉시 재계산이 필요한 소비자가 없다는 판단. 유일하게 "즉시 반응해야 하는" 소비자는 store-bind @@ -571,14 +571,14 @@ Fusion식 eager 노드·생성순 정렬은 안 만듦** **전역 원칙으로 명문화: "관측해야 실체화된다" (2026-08-04 세션)** 위 pull-recompute 규칙을 State 하나의 재계산 메커니즘으로만 읽지 말고, -프로젝트 전역에 적용되는 원칙으로 명시함: **어떤 파생값도 `.value`/`Get()`로 +프로젝트 전역에 적용되는 원칙으로 명시함: **어떤 파생값도 `:Get()`으로 직접 읽히기(관측) 전까지는 계산되지 않는다.** 이 원칙은 State 자체뿐 아니라, State를 필드 값으로 담고 있는 다른 구조(예: `base/modifier-plan.md`의 Modifier)에도 그대로 적용됨 — Modifier의 getter가 State 필드를 읽으면 그 순간이 바로 관측이고, 그 순간 계산이 확정됨. **주의 — 구조적 복사는 관측이 아님.** `table.clone`처럼 테이블 레퍼런스만 -복사하는 연산은 안에 담긴 State 핸들을 그대로 옮길 뿐 `.value`/`Get()`을 +복사하는 연산은 안에 담긴 State 핸들을 그대로 옮길 뿐 `:Get()`을 호출하지 않으므로 관측이 아니고, 계산을 트리거하지 않음. Modifier 체이닝 메소드가 `table.clone` 후 필드를 덮어쓰는 것(위 "Immutable 값 + clone 기반 체이닝")과 이 원칙이 충돌하지 않는 이유가 바로 이것 — clone은 그저 참조 @@ -625,23 +625,31 @@ Modifier처럼 플래튼하지 않는가"는 설계 근거를 알고 싶은 사 단점이 있었음 — self가 raw 값이면 `fn` 호출 전에 항상 self를 먼저 `Get()`해야 하므로, `fn` 내부 로직이 with한 다른 값을 보고 "이 경우엔 self 계산 자체가 필요 없다"고 판단해도 이미 늦음(예: `:With(noprint)`이고 - `noprint.value == true`면 앞단 계산을 통째로 생략하고 싶은 경우). + `noprint:Get() == true`면 앞단 계산을 통째로 생략하고 싶은 경우). - **해결(사용자 확정)**: self도 raw 값이 아니라 **State 핸들 그 자체**를 `fn`의 포지셔널 인자로 넘긴다 — `fn(self: State)`, 내부에서 - `self.value`(또는 `self:Get()`)를 실제로 읽을 때만 계산이 트리거됨. - with한 값과 동일한 lazy 원칙을 self에도 그대로 적용 — 별도 - `ComputeWithout` 변형은 불필요, `Compute` 하나로 일관. -- `.value`는 `Get()`을 감싼 읽기 전용 계산 속성(`base/lifecycle-pattern.md`의 - `Connected`와 동일한 "저장되는 필드가 아니라 계산된 속성" 패턴 재사용) — - `:Get()`과 `.value` 둘 다 지원, `.value`가 관용적 표기. + `self:Get()`을 실제로 읽을 때만 계산이 트리거됨. with한 값과 동일한 + lazy 원칙을 self에도 그대로 적용 — 별도 `ComputeWithout` 변형은 + 불필요, `Compute` 하나로 일관. +- **[정정, 2026-08-07] `.value`는 State/Source에서 제외, `:Get()`만 지원.** + 이전엔 `Get()`을 감싼 읽기 전용 계산 속성(`base/lifecycle-pattern.md`의 + `Connected`와 동일한 "저장되는 필드가 아니라 계산된 속성" 패턴)으로 + `.value`/`:Get()` 둘 다 지원하고 `.value`를 관용적 표기로 앞세웠으나, + "관측해야 실체화된다"는 원칙이 가장 날카롭게 느껴져야 할 지점에서 + 프로퍼티 문법이 그 느낌을 무디게 한다는 재검토 끝에 함수 호출 + `:Get()` 하나로 좁힘 — `:Set()`과의 동사 짝도 자연스러움. `.value` + 표기 자체는 폐기하지 않고 **Ref 전용으로 좁힘**(Ref는 lazy가 아니라 + 값을 읽어도 계산이 트리거되지 않으므로 프로퍼티 문법이 정직함 — 이 + 절 위쪽 "Ref 일반화" 절의 `.Value`가 그대로 유일한 존재가 됨, 이름 + 충돌 자체가 사라져 별도 표기 정리 불필요). - 예시 갱신: `store "key1":With(store "key2"):Compute(function(key1) return - key1.value + store.key2.value end)` — `key1`은 이제 raw 숫자가 아니라 + key1:Get() + store.key2:Get() end)` — `key1`은 이제 raw 숫자가 아니라 State. **State는 쓰기 대상이 아님 — 확정, Source는 독립 공개 프리미티브로 격상** -- `.value`는 항상 읽기 전용. State에는 쓰기 API가 아예 없음. "State에 - `.value = x`를 허용하면 다른 source에서 파생된 state에 직접 쓰기가 +- `state:Get()`은 항상 읽기 전용. State에는 쓰기 API가 아예 없음. "State에 + 직접 쓰기 API를 허용하면 다른 source에서 파생된 state에 직접 쓰기가 가능해져 버린다"는 이전 우려는 이걸로 근본적으로 해소(그런 API 자체가 없음). - **[정정, 2026-08-06 후속 세션] 값을 쓰는 경로는 `store.key = value` diff --git a/.claude/base/store-semantics.md b/.claude/base/store-semantics.md index c6ad017..18372c5 100644 --- a/.claude/base/store-semantics.md +++ b/.claude/base/store-semantics.md @@ -108,9 +108,12 @@ State를 만족하도록 만들고, RefSource라는 별도 타입은 폐기**하 **확정 방향**: - **`Source`가 구조적으로 `State`를 만족(단방향 호환)** — State 자리엔 Source를 넣을 수 있지만 역은 안 됨(Svelte의 `Writable extends - Readable`와 같은 모양). Source는 State가 주는 모든 것(`.value`, - `:Get()`, `:With(...)`, `:Compute(fn)`) 위에 `:Set(value)`/`:Emit()`을 - 추가로 가짐. + Readable`와 같은 모양). Source는 State가 주는 모든 것(`:Get()`, + `:With(...)`, `:Compute(fn)`) 위에 `:Set(value)`/`:Emit()`을 + 추가로 가짐([정정, 2026-08-07] `.value`는 State/Source에서 제외되고 + `Get()`으로 통일됨, `.value` 표기는 Ref 전용으로 좁혀짐 — `base/ + bind-system-plan.md` "`:With`/`:Compute` — self 인자도 lazy 핸들로 + 통일" 절 참고). - **`:With`/`:Compute`는 Source에서도 항상 `State`를 반환** — Source 자신을 변형하는 게 아니라, "Source의 State 뷰를 뽑아 그 위에 파이핑"하는 것과 동치. 구현은 metatable `__index` 델리게이션(Source의 메소드 @@ -287,4 +290,4 @@ Source가 State 계약을 만족하는 이상 같은 이유(Modifier용 processo 연산.) 최종 형태는 `:With(...)`로 의존성을 모으고 `:Compute(fn)`으로 파생 State를 만드는 것으로 확정 — `Store.Combine({a,b}, fn)`류 포지셔널 인자 방식은 기각됨. 정확한 lazy 인자 규칙(self/with 값 둘 다 -State 핸들로 넘기고 `.value`를 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. +State 핸들로 넘기고 `:Get()`을 실제로 읽을 때만 계산)은 `base/bind-system-plan.md`의 "Store/State/Source 온톨로지" 절 참고. diff --git a/.claude/question.md b/.claude/question.md index 525d2e4..2acd39e 100644 --- a/.claude/question.md +++ b/.claude/question.md @@ -92,16 +92,6 @@ additional-primitives-plan.md`. 요지: 주기가 없다"는 기존 원칙과 이름이 충돌해 보일 수 있음), `Draw`(짧지만 즉시모드 GUI 뉘앙스), `List`(중립적이나 메커니즘을 안 알려줌) — 아직 미정, `research/additional-primitives-plan.md` 1번 절 참고. -- **State/Source 읽기 접근자 — 함수명은 `Get()`으로 확정(2026-08-07), - `.value` 존치 여부만 남음**: `Get()` vs `Pull()` 중 `Get()`으로 확정 — - 둘 다 "명시적 게터 호출"이라는 엔지니어링 기준에서 동급이라 승부처가 - 아니었고, 이미 문서 전체가 `Get()`을 쓰고 있어 바꿀 이유가 약함(`Finalize`는 - 앞서 quad의 "정리/해제" 어휘(Effect leaf-death cleanup, `retract`, - `Destroying`)와 충돌 위험으로 이미 기각). **아직 열려있는 건 `.value` - (Get()을 감싼 "관용적 표기" 계산 속성) 자체를 유지할지** — quad가 - 명문화한 "관측해야 실체화된다" 원칙을 프로퍼티 문법이 무디게 만드는 건 - 아닌지, 유지한다면 State/Source에도 둘지 아니면 진짜 non-lazy인 Ref에만 - 남길지. - **"프로바이더"(3순위, 사소함)**: `base/module-lifecycle-plan.md`가 "provider"라고 불러온, `isHandlable`로 참여 여부를 결정하고 우선순위대로 스캔되는 pluggable 참가자 개념 — 정확한 이름을 "provider"/"processor"/ diff --git a/.claude/research/debug-tooling-plan.md b/.claude/research/debug-tooling-plan.md index 7425616..e56d26e 100644 --- a/.claude/research/debug-tooling-plan.md +++ b/.claude/research/debug-tooling-plan.md @@ -257,7 +257,7 @@ trace 이벤트 페이로드는 처음부터 함수/클로저 없이 **순수 ### 6. "관측해야 실체화된다" 원칙 — quad-debug 자신이 위반하면 안 됨 -`bind-system-plan.md`의 전역 원칙: 어떤 파생값도 `.value`/`Get()`으로 직접 +`bind-system-plan.md`의 전역 원칙: 어떤 파생값도 `:Get()`으로 직접 읽히기 전까지 계산되지 않음. quad-debug UI가 트리뷰를 그리면서 모든 State를 자동으로 펼쳐 값을 미리 읽어버리면, 원래 필요 없었을 계산을 디버그 도구가 유발하는 부작용이 생김 — **디버그 도구 자체도 lazy해야 함**: 사용자가 UI에서