Commit graph

252 commits

Author SHA1 Message Date
56ba2b3f37
docs(audit): 스파이크 상태판 신설 + 코퍼스 전반 모순·stale 감사 정정
## luau-test/STATUS.md 신설 (사용자 요청)

"사람이 보기엔 뭐를 바로 봐야하는지 눈에 안 띄어서" — 파일별 상태를
🔴 사람 결정 필요 / 🟠 스파이크 깨짐 /  미실행 /  통과로 분류하고
봐야 할 것을 위에 배치. 역할 분담도 명시: STATUS.md가 상태의 소스,
README.md는 각 파일의 검증 의도·배경, audit/는 실행 결과 상세.

## 코퍼스 전반 감사 (에이전트 둘, 보고받은 것 전부 직접 사실 확인 후 수정)

가장 위험: ROADMAP.md가 최우선 게이트(question.md 0-Z, 하강 diff 재설계)를
전혀 안 짚고 있었음. base 4개 문서엔 ⚠️ 배너를 달아뒀는데 정작 구현 순서의
소스인 ROADMAP엔 없어 M2/M4/M10 담당자가 옛 모델로 구현할 위험이 실재.
→ 세 마일스톤 전부에 배너/포인터 추가.

두 번째: slot-plan.md 앞부분이 뒤집힌 결정을 여전히 "확정"으로 자칭.
"폐기, 옮기지 않음, portal은 오버엔지니어링"과 "열린 질문" 절에 역전 표시가
없어 앞에서부터 읽는 구현자가 구 모델로 짤 위험 → 🔄 역전 배너.
더 심각하게 reconcile 의사코드가 여전히 rawRemove(파괴)를 부르고 있었음
(같은 문서가 "[반영 완료]"라 태그해둔 것과 정면 모순) → rawUnmount 신설.
filter/toggle 근거 절도 "제거=파괴" 전제라 캐비엇 추가(결론은 유효).

그 외:
- ROADMAP M11 Tween이 이미 확정된 넷을 미결로 둠(override 정책/옵션 값
  모양/Animate 시그니처/initValue) + research/tween-plan.md 죽은 링크 4곳
- bind-system-plan.md:328, architecture.md:213의 "4종 계약" 잔여
  (isHandlable(k,v) 구식 시그니처 포함)
- store-semantics.md의 "State는 가칭" stale — 2026-08-12에 최종 확정됨
- luau-test/README.md의 04/19 판정 기준이 재작성된 파일을 못 따라감.
  특히 19 C섹션 기준이 정상 동작(nested error)을 실패로 오판하게 돼 있었음
- documentation-content-map.md의 4종 계약 / Attribute<T> 옛 이름
- modifier-plan.md의 "정정이 원문에 소급 안 됨" 패턴
- attribute-plan.md "이름 소유권"이 0-Z 미결인데 "최종"이라 적힌 것 캐비엇
- relate-plan.md의 kSlotMap 역할 서술 부정확, 상호참조 방향 오류 2곳

판단해서 안 고친 것: effect-plan.md의 "cleanup" 14곳 — Handler retract
(내부 배관)와 달리 Effect(fn)에서 사용자가 작성한 fn이 반환하는 React식
콜백이라 다른 층위. 매 감사마다 재지적되므로 lifecycle-pattern.md에
"확인 완료, 의도된 별개 개념"으로 못박음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:48:32 +09:00
b90b3a6b94
fix(slot,docs): 리뷰 지적 5건 정정 — 언마운트/파괴 모순, 계약 개수 모순, 해제 순서 역전
다른 에이전트 리뷰가 짚은 5건을 전부 사실 확인 후 수정.

1. (가장 심각) slot-plan.md의 SlotHandler.process 반환 클로저가 여전히
   destroySlotTree를 불러, 같은 커밋에서 확정한 "State<Slot> 교체는 파괴가
   아니라 언마운트" 결정과 정면 모순 — 그 결정이 적용돼야 할 최상위 dispatch
   경로가 바로 이 클로저인데 "구현상 바뀌어야 하는 것" 절이 reconcile만
   언급하고 빠뜨렸음. 그대로 뒀으면 언마운트 결정 자체가 무의미해질 뻔함.
   → 비파괴 경로 unmountSlotTree 신설(destroySlotTree와의 차이는 딱 둘:
     실제 Destroy 안 함, 자식 releaseOwner 안 함 — 자식이 계속 그 slot
     소유라 통째로 재마운트 가능 = 포탈). 클로저를 unmountSlotTree +
     setOffsetSource(None) -> setLength(0) + unbindLifetime + releaseOwner로
     교체. "자동 경로는 언마운트, 명시적 Remove/Clear/dispose만 파괴"로 정리.

2. bind-system-plan.md "확정된 것" 절이 폐기된 4-메소드 계약을 여전히 확정
   사실로 서술 — 같은 파일 상단은 이미 3-메소드로 갱신돼 자기 문서 내 모순.

3. question.md의 Slot 해제 순서가 거꾸로 — setLength(0)을 먼저 부르라고 돼
   있었는데, 같은 커밋이 slot-plan.md에 추가한 경고가 정확히 그 순서가
   죽어가는 서브트리 Source에 헛된 :Set()을 날린다고 명시.

4. ROADMAP.md M6에 이번 세션 Slot 결정이 전혀 미반영(M0/M2/M4/M10엔 반영됨)
   — 언마운트 전환/dispose/claimOwner vs claimOwnerAt 분리/버그 4건 추가.

5. CLAUDE.md 여섯 번째 세션 항목이 자체 규칙(2~4줄+링크) 위반, 63줄까지
   불어남 — 과거 3000줄 비대화를 유발한 패턴 재발. 15줄로 압축.

6번(StoreBind가 최초 발화에서도 retractFrom을 부르는 것)은 사용자 판단으로
그대로 둠 — 빈 슬롯 retractFrom은 무시 가능하고 일관성이 낫다는 결정.

추가: question.md 0-Y에 사용자 방향 기록 — 순환 타입을 만들기보다 State 타입
자체를 구울 때 인라이닝하는 쪽(Modifier flat 타입 생성과 같은 결). 사람이
직접 확인할 부분이라 0-Z(Attribute)와 함께 사용자 판단 목록으로 이관.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:32:04 +09:00
124b706e2c
test(luau-test): 첫 실측 라운드 — 런타임 12개 전원 통과, 타입 이슈 1건 발견
luau/luau-analyze 바이너리가 사용 가능해져, 2026-08-09에 스파이크를 만들기
시작한 이래 처음으로 실제로 돌림(CLAUDE.md가 "M0 착수 전 남은 유일한
게이트"로 꼽아온 항목). 전문: .claude/audit/luau-test-first-run-2026-08-13.md

## 설계 검증 결과 — 런타임은 전부 성립

- 04가 직전 커밋의 감사에서 찾은 버그를 음성 대조군으로 재현: chains:SetStrong을
  process 뒤에 두면 체인 깊이가 3 대신 1로 무너지고, 죽은 store가 나중에 UI를
  덮어씀(STALE). 감사→수정 사이클이 실측으로 닫힘.
- 07은 보강해야 실제 검증이 됐음. 3번 섹션이 sanity check만 하고 있었고 헤더가
  내세운 연쇄 GC 주장은 미검증이었음. 파일이 스스로 적어둔 "weak table 엔트리를
  셀 표준 API가 없다"는 전제도 틀렸음(outer가 __mode="k"라 GC 후 pairs에서
  사라짐) — _countEntries + weak-value canary로 4번 섹션 신설, GC-native
  아키텍처의 핵심 전제(연쇄 GC)가 실측 확정됨.
- 18이 relate-plan.md의 상호 순환 경고를 실증 — 추측이 아니라 실제로 GC 안 됨.
- 01/02/03/05/06/20도 전부 통과.

## 타입 — 진짜 설계 이슈 1건 (question.md 0-Y 신설)

:Compute(fn)의 lazy 핸들 계약이 Luau 양방향 추론과 충돌.
`state:Compute(function(s) return s:Get() * 2 end)`가 타입 에러를 냄.
최소 재현으로 원인 확정: read/self 표기 조정으로는 안 풀리고, 콜백이 raw 값을
받으면 완전 클린. Effect/Observer/Animate/Operator 등 같은 계약을 공유하는
API 전부에 걸림 — M0 착수 전 결정 필요.

## 문서 결함 발견 — modifier-plan.md

"데이터를 테이블에 직접 두고"가 "self 최상위 리터럴 키"로 읽힐 여지가 있었는데,
그렇게 하면 __index가 rawget 성공 시 안 불려 같은 필드 재호출이 죽음. 그 재호출
패턴이 문서 3·4절의 대표 용례라 실사용에서 즉시 터지는 경로였음 — 경고 문단 추가.

## 스파이크 수정

- 17: self 최상위 키 저장 → 내부 저장소 구조로 재작성(크래시 해소)
- 11: 브랜드 판별 크래시로 "다른 Modifier" 케이스가 엉뚱하게 통과하던 것 수정
- 19: B/C를 폐기 설계(rawNew+owners, 3분기 claimOwner)에서 현행 설계로 재작성,
      음성 대조군으로 옛 로직이 Slot{a,a}/Frame{slot,slot}을 통과시킴을 재현
- 07: 위 참고

남은 것은 13/15/16의 스파이크 격리·API 재확인(설계 문제 아님).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:19:00 +09:00
8c784288b0
audit(dispatch,slot): c33ae04 전체 감사 — 실제 버그 4건 수정, Slot 언마운트 전환, 재디스패치 모델 재설계안
c33ae04(인덱스 기반 Dispatch 재설계) 전체를 직접 정독. 방향은 옳으나 새로 쓴
의사코드가 손 트레이싱을 안 거친 채 커밋됐음이 드러나 실제 버그 4건 발견·수정:

1. Dispatch.process가 chains:SetStrong을 h.process 뒤에 둬서 최초 마운트에서
   하위 위임 retractor가 통째로 유실(재귀가 자기 테이블을 만들었다 바깥이
   덮어씀 — Slot이면 서브트리 중복 마운트로 직결). SetStrong hoist + 재귀 중
   hole 방지용 no-op 점유 마커 추가.
2. Attribute 그룹이 process에서 retractFrom을 선행 호출해 인덱스 1이 무조건
   비워지는 바람에 점유 체크(=소유권 충돌 감지)가 전혀 작동 안 함. 철거를
   반환 클로저로 이동.
3. SlotHandler가 claim 실패에도 파괴적 클로저를 반환 — nested는 엄격
   claimOwner, top-level은 claimOwnerAt(inst,k)로 분리. rawRemove의
   releaseOwner 누락, destroySlotTree의 GC 타이밍 의존 오류도 수정.
4. Ref retractor가 spurious 재발행에서도 relate를 지워 dedup 무력화.

사용자 설계 결정:
- State<Slot> 교체 = 파괴가 아니라 언마운트(state<Frame>와 동일). 포탈이
  별도 기능이 아니라 이 결정의 귀결이 됨. 해제는 setOffsetSource(None) →
  setLength(0) 순서 고정(반대면 죽는 중인 서브트리 Source에 헛된 Set이
  날아감). dispose(value) 신설 — 트리가 아직 요구하면 파괴 거부하고 error.
- 재디스패치를 "하강 diff"로 재설계 — 래핑 핸들러의 retractFrom 선행 호출을
  폐기하고 Dispatch.process가 핸들러를 먼저 비교. 힌트가 None/래퍼로 오염돼
  깜빡임 방지가 조용히 꺼지는 결함이 계기. 아직 research/에만 있고 base
  미반영(Attribute 이름 소유권 하나가 남음) — base 4개 문서에 경고 배너.

전파 누락 정리: ROADMAP.md가 2026-08-08 이전 모델로 남아있던 것, base 내
"3종 vs 4종 계약" 모순 6개 문서, luau-test/04가 없어진 가드를 검증하던 것
(04는 인덱스 모델 + 버그 1 재현 음성 대조군으로 전면 재작성).

재발 방지: bind-system-plan.md "Handler 작성 체크리스트"(7항목),
relate-plan.md "언제 Relate를 쓰고 언제 쓰면 안 되는가" 신설.

다음 세션 최우선: question.md 0-Z(Attribute 이름 소유권) — 사용자가 직접
스케치하며 심층 분석 예정.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 16:01:49 +09:00
c33ae041b0
design(dispatch): 사각지대 손 트레이싱 라운드 + Dispatch 인덱스 기반 전면 재설계
네 번째 세션: State<State<T>> 발견 방식을 서브에이전트로 코퍼스 전체에
반복해 Tag 참조 카운트(객체 identity 키잉)/Attribute 그룹 자기충돌 버그
발견·수정. Attribute 소유권 충돌 방지를 위해 처음엔 Dispatch.processAs/
retractSelfAndUnder 체크포인트 핸들러 패턴을 신설.

다섯 번째 세션: 체크포인트 패턴이 임시방편이라는 지적에서 출발해, chains의
핸들러 객체 identity 기반 추적 자체가 State<State<T>>를 UB로 만든 근본
원인이었음을 재확인 — 재귀 깊이 인덱스로 재설계(같은 키 재귀는 index+1,
다른 키 위임은 항상 1부터), Handler 계약을 process/retract 2-메소드에서
process가 자기 retract 클로저를 반환하는 1-메소드로 축소. 이걸로
State<State<T>>가 UB에서 정상 지원 대상으로 바뀌고, 전날 만든 체크포인트
패턴 전체가 불필요해져 archive로 이전. 여러 핸들러(StoreBind/Ref/Tag/Slot/
Attribute)의 private Relate 상태 저장소도 대거 정리됨.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132hkscPQWa34iyHrHdoRiF
2026-08-13 14:24:48 +09:00
35c66d4cd0
decide(v1-compat): objectListClass.__newIndex 오타 기능, v2 무관으로 스코프 아웃
v1 재현 테스트가 필요하다고 남아있던 question.md 항목을 사용자가 정리 —
당시 실수였던 건 맞지만 v2는 오브젝트 id 주입/조회 개념 자체가 없어져
재현 여부와 무관하게 v2 마이그레이션 가이드에서 다룰 대상이 아님.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:55:27 +09:00
6e9f6fefa1
fix(dispatch): State<State<T>> 재진입 디스패치 체인 파손 버그 발견·수정, Operator Alternative 후보 신설
Haskell Monad/Applicative 비교 리서치 중 사용자가 retractUnder의 꼬리부터
cutoff 로직을 재검토하다 제기한 의심을 pseudocode 손 트레이싱으로 확인 —
store가 emit하는 값 자체가 또 State/Source면 같은 (inst,k)에 같은 핸들러가
중복 push되어 안쪽 구독이 등록 직후 스스로 끊기는 실제 체인 파손 버그였음.
Dispatch.process에 중복 핸들러 즉시 error 가드를 추가하고, 같은 시나리오를
이미 다루고 있었지만 no-op retract 스텁 때문에 증상을 못 잡던 luau-test/04의
사각지대도 재작성. operator-sugar-plan.md에 Alternative(nil 대체값) 후보 신설.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:49:34 +09:00
5e6498a3e1
audit(lifecycle): partial gcconn trick verification, close Relate cycle spike gap
User confirmed the two Roblox-engine-dependent assumptions behind the
gcconn trick (ClassName signal never fires, Connection.Connected flips
synchronously on Destroy) via a Studio script; record what's verified vs
still open in a new audit/ folder and document the GC-trigger technique
used. While re-auditing the corpus, found relate-plan.md's ephemeron/
mutual-cycle claim was never actually run in Luau, and CLAUDE.md's
open-questions list still listed State as unresolved after it was
finalized in session 20 — add spike 18 and fix the stale entry. A
background sweep synced .claude/README.md's summary rows against
sessions 8-21 and added spikes 19/20 for newer ownership/refcount
mechanisms lacking coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGWwZ8khc3Zq7DAnd4Uw6a
2026-08-13 10:09:47 +09:00
2688409ebd
decide(state,operator,ref): use-after-destroy/동적With 의도적 비지원 확정, Operator 카탈로그 외부 리서치 반영, State 이름 최종 확정
- use-after-destroy 검증 안전망: rbvm 영역/quad-debug 스코프 밖으로 최종
  기각, Ref 사용 관례(useRef급 스코프) 명문화
- :With 동적 의존성: State immutable 가정과 모순되어 의도적 비지원 확정
- Operator 콤비네이터 카탈로그: 서브 에이전트 외부 리서치로 포함 범위/
  네임스페이스 이름 근거 보강(Clamp/Min/Max 추가 후보, 비트/비교/Sub/Div
  드랍 후보, Debounce/Throttle 별도 질문으로 분리)
- State 용어 정리 최종 확정(현재 이름 유지), Pipe 기각 근거·Compute vs
  Computed 네이밍 근거 문서화, :With와 Tag/Modifier clone 체이닝 혼동
  방지 경고 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVG74qV2nQVykhvMRQW2UC
2026-08-12 21:14:15 +09:00
4c5ee1b66a
decide(dispatch,modifier,store): pre-implementation-audit 우선순위1 마지막 넷 해소, 누락 스파이크 보강
사용자가 우선순위 동률/매치실패(1-3), provider 미주입(1-4), store.key
type function 타이핑(1-10), Modifier __index+table.clone 트릭(1-11)에
대해 구체적 결정을 제시 - 전부 base/ 문서에 반영해 우선순위1 11개 전원
해소. 핸드오버 점검 중 1-10/1-11이 설계 레벨로만 확인됐고 실제 Luau
스파이크 파일이 없었던 갭을 발견해 16/17 신규 추가, ROADMAP.md M2/M3/M7
체크리스트 누락분도 보강.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVG74qV2nQVykhvMRQW2UC
2026-08-12 20:24:04 +09:00
2df591851f
fix(attribute,slot): retract 전면 재설계, 소유권 레지스트리 통합, 코퍼스 감사
- Attribute: AttributeKeyHandler.retract를 완전 no-op으로 재정정 —
  지우는 건 오직 명시적 None/nil(process)로만, 그룹이 사라진 이름을
  자동으로 SetAttribute(nil) 안 해줌(Ref의 "Destroy 무관" 철학과 통일).
  단 사라진 이름의 StoreBind 구독은 retractUnder로 끊어 리소스 누수 방지.
- Slot: slotOwner를 elementOwner로 일반화해 top-level Dispatch/nested
  Add 경로가 같은 소유권 레지스트리를 쓰도록 통합(이중 마운트 gap 폐쇄).
  bindLifetime을 top-level 전용으로 축소(nested는 _elements 강참조로
  transitively 생존).
- 일반 규칙 신설(bind-system-plan.md): retract의 v는 타입 미보장이라
  내용을 보려면 isX(v) 가드 필수, retract 안에서 process 호출은
  retractUnder 체인 추적을 꼬는 UB.
- and/or 삼항 관용구 전면 금지(기존 "항상-truthy면 예외" 조항 폐기),
  코퍼스 전체 실제 코드 6곳을 if-then-else로 교체.
- 7-에이전트 코퍼스 감사로 stale 서술 다수 정정: retract-always-fires
  정정 전파 누락(bind-system-plan.md Tag 예시), Tween research→base
  승격 반영 누락(architecture.md/ui-shorthand-plan.md), Relate API
  인자 개수 버그(slot-plan.md), pre-implementation-audit.md 열린 항목
  개수 오류(question.md/CLAUDE.md).
- 백로그: Attribute 자동 unset용 :Apply 유틸 아이디어를
  research/operator-sugar-plan.md에 추가(착수 안 함).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVG74qV2nQVykhvMRQW2UC
2026-08-12 18:39:45 +09:00
b91c662f50
fix(slot): Splice가 vararg인 진짜 이유는 T가 opaque 제네릭이라서
직전 커밋이 "Slot의 T(Instance|Slot)가 우연히 테이블이라 T|{T}가
모호하다"고 적었는데, 이는 quad-roblox라는 특정 백엔드의 구체적 T에
기댄 특수 사례일 뿐 근본 원인이 아님 — 정확한 이유는 Slot<T>가 base
레벨에선 T가 뭔지 전혀 모르는 제네릭(다른 백엔드면 테이블/userdata/
그 밖에 뭐든 가능)이라, 바깥 {}가 단일 T인지 {T} 배열인지 원천적으로
판별 불가능하다는 것.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 17:14:15 +09:00
35306ed5ca
decide(tag,slot): Splice는 vararg 유지, Tag:Added 재정 근거 보강
Tag:Added가 string|{string}로 간 진짜 이유를 Lua table.unpack의 tail
위치 제약(뒤에 다른 인자/unpack이 오면 첫 값 하나로 잘림)으로 정확히
보강. 같은 논거가 Slot:Splice의 newElements에도 적용되는지 검토했으나
기각 — 사용 패턴이 소수 고정이고 동적이면 Slot-in-Slot으로 흡수 가능,
게다가 Slot의 T 자체가 테이블이라 T|{T}가 오히려 모호해짐 — vararg 유지로 확정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 17:10:00 +09:00
fda045a183
decide(tag): Added/Removed vararg -> string|{string} 재정정
동적으로 조립한 이름 목록을 vararg에 넘기려면 table.unpack이 필요해
번거롭다는 지적 — string 단일 이름 또는 {string} 배열을 받아 내부에서
flatten하는 쪽으로 같은 세션 내 재수렴. Tag(...) 생성자는 정적 호출
자리라 vararg 유지.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:59:53 +09:00
94978c78f4
decide(tag,slot): Tag:Added/Removed vararg fix, Slot:Splice CRUD 신설
Tag:Added/:Removed가 문서상 단일 name만 받던 불일치를 vararg로 정정
(self-return 최적화는 멤버십을 매번 먼저 읽어야 해서 기각). Slot:Splice
CRUD 신설 — 구간 제거+삽입을 shift/recompute 1회로 묶는 순수 최적화.
Slot-in-Slot relate 범위/Animate 반환타입/Slot retract 파괴는 기존
문서와 일치 확인만.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:56:47 +09:00
81ff240a59
decide(relate): confirm and document that Luau has no ephemeron tables
User cited https://luau.org/compatibility/ (Lua 5.2 section, "Ephemeron
tables") -- Luau explicitly did not adopt this due to complexity. Upgrades
the previous session's defensive "unverified, avoid just in case" framing
to a confirmed fact: two separate Relates strongly cross-referencing each
other's keys is a real, unrecoverable leak in Luau, not just a theoretical
risk. Adds a general rule to relate-plan.md so future Relate designs don't
have to rediscover this from scratch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:42:56 +09:00
95fdfc4b12
decide(slot): break the kSlotMap/slotOwner mutual strong-reference cycle
Two separate Relates strongly holding each other's key as their value
(inst->slot in one, slot->inst in the other) is a mutual-reachability
cycle that ordinary weak-key GC can't resolve on its own -- the case
Lua 5.2 ephemerons exist for. Weaken both to pure lookups and make
bindLifetime/unbindLifetime (already used for Slot's own observers)
the single GC anchor for the slot object itself, which attachSlot/
destroySlotTree were missing.

Audited every other Relate() in base/ for the same shape (an object
other than inst used as the outer key) -- slotOwner was the only one;
everything else either has no back-reference to inst or is the safe
single-table self-reference shape bindLifetime already relies on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:37:40 +09:00
2b7e90cd92
decide(slot): track slot->inst ownership directly instead of position diffing
Position-keyed comparison only catches "did this exact slot spot change,"
not "is this same Slot object already mounted somewhere else" -- which is
exactly the double-mount invariant Slot already promises for its elements.
A Relate<slot, inst> enforces it directly: same owner -> spurious re-emit,
ignore; different owner -> error; no owner -> bind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:20:29 +09:00
f20922ce39
decide(bind-system): retract fires on every re-dispatch, not just type changes
Corrects a foundational error: "handler type unchanged -> retract skipped,
process diffs" was never actually true. StoreBind unconditionally calls
Dispatch.retractUnder before every re-dispatch (already stated in the
original dispatch model doc) -- Tag's old assert(v==nil) was taken at
face value and the general rule was wrongly back-derived from it.

Tag, Ref, Slot, and Attribute (the last three designed earlier this same
session) all inherited the flawed premise. Fixes across all four:

- Tag: rewritten around kTagMap/tagNameMap reference counting so
  AddTag is entirely process's job, RemoveTag entirely retract's,
  using a Contains hint to skip engine calls that would be immediately
  redone. Also fixes the case where two independent Tag(...) values at
  different positions share a name (union semantics, like className).
- Ref/Slot: drop the incorrect assert(v==nil), split into "retract
  unbinds/destroys the old, process binds/mounts the new" with an
  identity check on both sides to avoid spurious re-fire.
- Attribute: AttributeKeyHandler.retract gated on v==nil so ordinary
  value updates don't flicker the attribute to nil; the group's
  "unchanged name" delegation now also retracts before reprocessing,
  since skipping it was silently stacking chain entries every cycle.

Reversal preserved in archive/retract-always-fires-reversed.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 16:13:25 +09:00
6b7af54e0d
decide(attribute): per-name ownership via private rawNew keys, not a new registry
Group Attribute(...) delegating through the public memoized AttributeKey(name)
cache meant a direct [AttributeKey "name"]=v write and a group field (or two
independent groups) targeting the same name silently converged on the same
dispatch slot -- last write wins, no error. Fix: groups use rawNew(name) to
mint a private per-name key cached on their own (inst,index) relation instead
of the shared cache, so AttributeKeyHandler can detect a conflicting claimant
by simple object-identity comparison -- no separate ownership registry needed.
Persisting names reuse the same cached key across group value swaps, so the
existing diff (nil only what left, leave the rest alone) keeps working.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 15:23:24 +09:00
27adad9c1a
decide(slot): store-bound Slot rebind uses Relate identity check, not retract
Same gap as Ref: slotA -> slotB via State<Slot> matches the same
SlotHandler both times, so Dispatch skips retract and process must
diff. Slot's confirmed no-portal/discard-only policy makes an identity
short-circuit a correctness requirement, not just an optimization --
without it, any spurious same-value re-emit would tear down and
rebuild the entire mounted subtree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 13:49:58 +09:00
a985874ca9
decide(bind-system): Ref retract mirrors TagHandler, non-nilable T stays valid
State<Ref> rebinds (refA -> refB) go through process's own diff (same
handler type matches both), not retract -- exactly the TagHandler
precedent. retract only fires when the slot stops being a Ref at all.
Both paths converge on old:Set(nil). Non-nilable Ref<T> remains a valid
"read a settled value" use case; callers opt into Ref<T?> explicitly for
Store/Modifier slots. Unbinding is independent of Instance Destroy --
that's Effect's job, not Ref's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 13:44:31 +09:00
cdbbdba4fb
decide(bind-system): PreRef is single-use, no cancel concept, reuse errors
PreRef never enters the normal retract dispatch chain (consumed via None
in the pre-pass), so "cancel" was never structurally possible. The real
hazard is reuse across constructions (stale .Value silently firing
callbacks) — guard it with an explicit error on re-fire instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 13:26:46 +09:00
e6880e518c
decide(tween): confirm natural-completion bookkeeping is left as-is, promote to base/
Completed 시 per-instance 북키핑 정리는 불필요 — 자연완료는 유저가 원한
목표값에 도달한 상태라 남은 참조가 부작용 없고, Value가 lerp 가능한
프리미티브라 메모리 문제도 없어 별도 정리 장치는 오버엔지니어링. 이걸로
tween-plan.md에 남은 열린 질문이 없어져 research/에서 base/로 승격,
라이브 크로스레퍼런스 전부 갱신.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 12:19:49 +09:00
3e37d9bc36
decide(operator): add Concat/Sorted/Filtered collection combinator candidates
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 12:13:38 +09:00
9909aca227
decide(operator): add Operator sugar plan, unify combinators on :Apply
Sketch Sum/Not/Product-style :Compute/:Apply sugar (research/operator-sugar-plan.md,
implementation deferred). Reusable curried combinators (Sum, Animate) must
go through :Apply, not :Compute — quad rejected implicit auto-tracking, so
a factory's closed-over deps only register if the factory re-declares them
via self:Compute(...) internally; plugging a pre-built factory straight
into :Compute silently drops reactivity. Flip Animate's call site
accordingly in tween-plan.md, add the convention to bind-system-plan.md's
:Apply section, and fix a stale two-arg Animate signature comment in
architecture.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 11:54:09 +09:00
34ded8b953
fix(docs): missing :Get() on :Compute callback args across base docs
state:Compute(fn)'s fn receives lazy State handles, not raw values, per
the confirmed self/with contract. Found and fixed the same class of bug
the user caught in the Animate example in three more spots (slot-plan.md
x2, tag-plan.md). Added a warning note near the contract since this is
an easy mistake to repeat.
2026-08-12 11:22:26 +09:00
a1f8601cc7
decide(tween): add Animate CanAnimate field, document Luau syntax facts
CanAnimate: State<boolean>|boolean|nil (nil defaults true) lets Animate
bypass wrapping in Tween{} entirely, covering the reduceMotion use case
natively. Also documents that Luau's if-then-else expression and const
bindings are official syntax (not hallucinated), so agents don't revert
them; const adoption deferred pending tooling support.
2026-08-12 11:17:49 +09:00
8fdb9f1bb2
decide(tween): finalize Animate combinator, fix and/or falsy-value bug
Animate(info) resolves T|State<T> option fields into a plain Tween{...},
matching :Compute's self-as-lazy-handle contract directly. Also audited
.claude/base for and/or ternary idioms per Luau if-then-else guidance;
fixed a real falsy-value leak in Dispatch.retractUnder.
2026-08-12 11:14:08 +09:00
eac66d0173
decide(tween): finalize option shape, override policy, relation-slot value
Info-first with TweenInfo.new() default fallback, plain-only option
fields, Cancel/Finish override collapse, initValue handed off to user.
2026-08-12 11:02:13 +09:00
6216f12f37
decide(attribute): group Attribute(...) primitive, AttributeKey rename, weak-cache identity
Adds Attribute(store1, store2, ...) as a Tag-shaped array-part value
object that projects one or more Stores' named Source fields onto
native Roblox attributes in one binding, with .Merged for combining
heterogeneous Stores. Rejected: a bare [Attribute] = Store{...} hash
slot (only one slot per instance, can't merge heterogeneous stores)
and making Attribute a Store subtype (would let Store<T>'s T be a
Store again, colliding with the existing "handler-layer values can't
live in Source" rule). Verified the value-slot aggregation in .Merged
doesn't reopen the earlier "layered Store" rejection (archive/
context-rejected.md) since it's an explicit one-time author-time
composition, not an implicit read-time parent-chain fallback.

Renamed the existing single-key constructor Attribute<<T>>(name) to
AttributeKey<<T>>(name) to remove the name collision with the new
group primitive (provisional per existing OnChange/OnChangeKey
precedent; final bikeshed still queued in question.md).

Follow-up: AttributeKey(name) now memoizes through a name-keyed weak
table, guaranteeing AttributeKey(a) == AttributeKey(a) while any
strong reference (e.g. a live Dispatch chain entry) keeps it alive.
This let the group Attribute handler drop its originally-planned
self-contained SetAttribute/subscription logic and instead recurse
into the existing single-key AttributeKey dispatch path per field,
reusing its None/retract/store-bind handling instead of duplicating
it — the group handler's own state shrinks to just a prior-name-set
diff for deciding which keys to retractUnder. The same memoization
was applied to OnChangeKey for the same reason (pure name-to-key
mapping, no other varying state), confirmed safe even once
State<function> callbacks are involved.

Reflected across base/attribute-plan.md, base/onchange-plan.md,
base/architecture.md (source tree + Brand isX list), ROADMAP.md M2/M10,
question.md, README.md, and the luau-test type-narrowing spike.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 18:22:54 +09:00
1f56c75978
chore(docs): split CLAUDE.md session log into .claude/session/, keep 2-4 line summaries
CLAUDE.md had grown to 3196 lines of accumulated session logs, causing
context bloat. Full session narratives (including trial-and-error and
later-corrected reasoning — quadnomicon devlog raw material) now live
as 39 individual files under .claude/session/. CLAUDE.md keeps only a
short "지금 할 일" (re-synced against question.md/pre-implementation-audit.md,
stale detail dropped) and a compact per-session summary+link table.
No design decisions changed; base/research/question.md were already
in sync with every session (verified against README.md/question.md
before archiving), so no unreflected content needed migrating first.
2026-08-11 14:40:43 +09:00
6ea3afa76a
decide(slot): reactive raw elements via Slot:Single sugar, :List index skip fix
Slot:Add now accepts State<T>/Source<T> elements directly, arbitrarily
nested. Implemented as pure sugar (isState(element) -> internally
Slot():Single(element)) rather than a new position-keyed StoreBind
mechanism, since the latter would reintroduce array-part None, Length
special-casing, and Move/Swap index-subscription sync bugs that :List's
key-based design specifically avoids. Slot:Single's updateFn is now
optional (defaults to identity) to support this sugar. Also fixes
:List's reconcile to skip the compact index by a nested-Slot result's
.Length instead of a flat +1, so multi-root list items don't produce
overlapping LayoutOrder ranges for subsequent siblings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ymPLYAQmEYnTrnrdAwxpU
2026-08-11 14:25:19 +09:00
cebef6d973
decide(slot): Slot:Single + Slot-in-Slot nesting, fix recompute off-by-one
Slot:Single confirmed as pure :List sugar (resolves the original
State<Frame?> offset-access motivation directly). Slot-in-Slot nesting
confirmed for component-composition uniformity: Dispatch.setLength/
setOffsetSource reused recursively keyed by the Slot object itself
(no new primitive), Slot.Length becomes a contribution-sum, teardown
is a flat destroySlotTree + explicit unbindLifetime instead of
recursive Clear(). Slot(initial?) constructor revived as pure :Add
sugar, with a new _crudUsed <-> _listed mutual-exclusion guard.

Also fixes a real off-by-one bug in the existing (pre-nesting)
Length/Offset recompute — offset was accumulating inclusive of its
own position instead of exclusive. A reentrancy guard was considered
and rejected: each Slot owns an independent bookkeeping table, so
nesting alone never re-enters the same one; genuine reentrancy is
now named UB under the same Source-to-State unidirectional-flow
principle already governing Source/State.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ymPLYAQmEYnTrnrdAwxpU
2026-08-11 13:55:31 +09:00
c02f52578f
decide(slot): frame Slot docs as a dynamic-rendering tool
Confirms Slot's documentation tone as "동적 렌더링을 가능하게 하는 도구"
rather than a static children-array description, extends the framing to
the still-unstarted :Single backlog item, and fixes an adjacent stale
marker (sibling Slot ordering was already resolved via Length/Offset).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 12:15:32 +09:00
895a17c212
decide(slot): Slot:List index/offset ownership moves to updateFn
- Slot no longer auto-binds LayoutOrder onto mounted elements (was
  magic/layering violation) - index (raw, compacted position) and
  offset (Slot.Offset, now a public field like Length) are passed to
  updateFn as plain values; actual property binding is entirely
  updateFn's responsibility via its own userdata-managed Source
- updateFn(item, index, offset, prev, userdata) param order now
  mirrors return order (prev, userdata) instead of the reversed one
- updateFn's three branches (discard/redraw/update-only) documented
  explicitly - only updateFn knows which applies, so deciding Set
  timing outside it wastes writes on soon-to-be-discarded Sources
- fix reconcile bug: rawAdd/rawMove was using the raw data-array loop
  index instead of a compacted mounted-position counter, which could
  error once filtering was in play; introduced candidateIndex
- add duplicate-key guard (seen[key] check, ~free) and explicit
  key/index terminology disambiguation (raw data index vs key vs
  updateFn's compacted index all get confused otherwise)
- sync ROADMAP.md M6 and .claude/README.md summaries to match

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 12:06:38 +09:00
b668109eaf
decide(bind-system): :Compute trailing deps as fn positional args, fix previous ordering
Expose :Compute(fn, ...)'s trailing deps to fn as lazy State-handle
positional args (removes the closure-capture duplication/drift risk in
curried fn factories). Confirms self's lazy-handle sugar generalizes
here because trailing args are local to one call, unlike :With chains.

previous must sit right after self, before the deps pack
(fn(self, previous?, ...deps)) — not after it as first proposed:
Luau's "..." must trail the parameter list, so a fixed arg after a
generic type pack is very likely unrepresentable. Adds luau-test 15
with a negative control (old broken ordering) and positive control
(corrected ordering) to verify, plus the remaining open question
(whether heterogeneous dep types survive one generic pack).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BBBW9GakG8J3CFumTSJ8bw
2026-08-11 10:52:20 +09:00
9370acd394
decide(bind-system): Compute(fn, ...) trailing-args sugar 확정
Compute 노드는 결과를 담을 새 State 노드를 어차피 만들어야 하므로
추가 의존성 구독을 그 노드에 얹는 건 공짜 sugar로 확정. Effect/Observer는
자기 자신이 State 노드가 아니라 다중 의존성 병합에 새 노드가 실제로
필요해서 동일 sugar를 의도적으로 제외, :With(...)를 코드에 그대로
노출하도록 유지.
2026-08-11 10:29:35 +09:00
32601487ae
chore: add empty folders 2026-08-10 01:22:53 +09:00
113b160b81
Merge remote-tracking branch 'origin/quad2' 2026-08-10 01:17:08 +09:00
fa2c7b900a
Cleanup repo 2026-08-10 01:13:19 +09:00
qwreey
4824bab145
Merge pull request #7 from qwreey/dev3-linker
Dev3 linker
2026-08-10 01:10:33 +09:00
2be4fffeca
decide(bind-system): OnChange 특수 키 신설 - GetPropertyChangedSignal 바인딩, 제네릭 없이 확정
이벤트 문자열 키 패턴이 GetPropertyChangedSignal엔 안 통해서(프로퍼티 이름이
값 세팅 키와 겹침) 별도 OnChange(name) DI 키를 신설. Attribute와 달리 제네릭
타입 파라미터 없이 콜백 타입은 인라인 명시 - 이벤트 바인딩과 같은 급의 타입
안전성 트레이드오프. 전부 quad-roblox 소속, State<function>은 기존 이벤트
store-bind 메커니즘 재사용. 프로퍼티별 정적 코드 생성 안은 규모 폭발로 기각.
2026-08-10 01:08:02 +09:00
7466e9216b
decide(tween): 값-레벨 Tween<T> 래퍼로 재설계, pre-implementation-audit 1-1 해소
독립 Dispatch 핸들러("v가 Store인 아무 k나 잡는 우선순위 최상위 핸들러")
모델을 PropertyHandler가 소비하는 값-레벨 래퍼(Tween<T>)로 전환. State/
Source 언랩(범용 StoreBind)과 "이 값이 트윈 대상인가" 판단을 분리해,
일반 반응형 프로퍼티 바인딩이 Tween 파일을 거쳐가는지 불명확했던 구조적
모호함(pre-implementation-audit.md 1-1)을 해소.

- Tween.Value는 plain T만(반응성은 바깥 :Compute가 전담, 이중 경로 방지)
- hasBeenSet+활성 엔진 트윈을 3-상태 릴레이션 슬롯(RobloxTween|true|nil)
  하나로 통합, 첫 세팅은 항상 애니메이션 없이 스냅
- 활성 트윈 정리 후에만 새 값 세팅(순서 뒤바뀌면 값이 덮어써질 위험)
- 타입은 T'=T|Tween<T> 치환만으로 기존 T|State<T> 모양에 자동 통합
- useTween은 :Apply(Animate(...))로 해소, 새 옵션 필드 불필요
- PropertyHandler가 항상 매치되는 유일한 핸들러가 되어 Tween↔프로퍼티
  handler-switch에 의존하던 retract 케이스가 사라짐

구 모델은 archive/tween-special-bind-key-reversed.md로 보존. 코퍼스
전체(bind-system-plan.md/architecture.md/modifier-plan.md/ROADMAP.md/
question.md/README.md/attribute-plan.md)의 stale Tween 참조 동기화,
CLAUDE.md 세션 요약 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 00:53:26 +09:00
fc43e1f267
Merge branch 'worktree-slot-parent-ub-doc'
동적 자식 추가/제거는 Slot/state<Frame>만 정당, 그 외는 UB로 명문화하는
문서 갭 보강 작업을 main에 통합.

# Conflicts:
#	CLAUDE.md
2026-08-10 00:14:49 +09:00
577277823a
docs(base): 동적 자식 추가/제거는 Slot/state<Frame>만 정당, 그 외는 UB로 명문화
Slot의 Length/Offset 카운팅이 Dispatch.setLength/setOffsetSource 호출에
의존하는데, 이 둘을 우회하는 경로(외부에서 직접 .Parent = inst로 자식을
끼워 넣는 것)가 UB라는 게 문서 어디에도 명시돼 있지 않았던 갭을 보강.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 00:11:12 +09:00
85e467152c
decide(base): Slot:Add가 삽입 인덱스 반환, 범위 밖 index는 clamp 대신 error
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 00:09:29 +09:00
df4a77b02d
docs(luau-test): M0 사전검증 스파이크 신설, luau-ignoreme→.claude/luau-test 이동
base/ 확정 사항 중 아직 실제 Luau로 부딪혀본 적 없는 것(M0 스파이크
대상)을 사용자가 luau/luau-analyze/luau-lsp/Roblox Studio로 직접
돌려볼 수 있는 독립 실행 스크립트 14개 + README 색인으로 정리. 커밋
f198fd9의 정정사항(Ref 콜백/대기자 배열 소진을 None에서 nil로 되돌린
것 등)을 반영해 02번을 재작성했고, 타입 관련 실측이 필요한 항목
(Attribute 제네릭 DI 키 narrowing, Ref/PreRef 구조적 서브타입, Source/
Ref nilable-default 오버로드)을 새로 찾아 12~14번으로 추가함. 처음엔
git 자동 제외 폴더(luau-ignoreme/)에 만들었으나 커밋해서 레포에
남기기로 해 .claude/luau-test/로 이동, .claude/README.md에 색인 추가.
CLAUDE.md에 세션 요약 반영 — 아직 실행 결과는 미확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-09 23:55:22 +09:00
f198fd9c6b
fix(base): 중간검토(질문 모드)에서 발견된 설계 결함 다수 수정
.claude/base/ 전체를 배치별로 리스팅해 사용자 확인을 받는 중간검토
세션 — Ref 콜백/대기자 배열의 None 소진이 무한 성장 버그였던 것을
nil로 되돌리고, isRef/isPreRef를 isState/isSource와 같은 상위-하위
합성으로 재정정, Slot CRUD 식별 기준을 element 레퍼런스에서 인덱스
기준으로 전환(ExtractAll/Get/IndexOf 신설), 컴포넌트 리프 바인딩에서
Source 직접 사용이 정상 경로라는 정정, Dispatch 직접 호출 UB 명시,
Tag retract 전제 명시, Attribute 타입 파라미터화 확정, EffectHandle
내부 Observer cascade/Subscribe GC 예외 경고 등을 반영. CLAUDE.md에
세션 요약, stale해진 research/documentation-content-map.md 일부 항목도
동기화.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYfAz3BsaTrmMM8hnzn6mj
2026-08-09 23:37:46 +09:00
5836c2d12a
decide(base): Slot:List의 data:Observer 구독을 마운트 시점 lazy bindLifetime으로 통일
Dispatch.setLength는 이미 마운트 시점에 bindLifetime을 걸었지만, Slot:List의
data:Observer(fn) 구독은 :List() 호출 시점(inst를 모르는 시점)에 즉시
생성되어 Destroy 후에도 재실행/관측을 멈출 방법이 없었음. :List()는 이제
설정만 저장하고, 실제 구독+최초 reconcile은 Slot 마운트 시점에 activateList로
수행 — 마운트 이후 :List() 호출 시 self._mounted로 즉시 활성화.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYfAz3BsaTrmMM8hnzn6mj
2026-08-09 21:42:24 +09:00