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.
This commit is contained in:
qwreey 2026-08-12 11:17:49 +09:00
parent 8fdb9f1bb2
commit a1f8601cc7
Signed by: qwreey
GPG key ID: D28DB79297A214BD
4 changed files with 133 additions and 18 deletions

View file

@ -220,6 +220,26 @@ Tween/existing-instance-bind는 여전히 `research/`에 남아있고 이 구조
`base/bind-system-plan.md`의 "Dispatch는 프리미티브가 아니다" 절/
`base/store-semantics.md`의 "세 번째 카테고리 — Handler" 절 참고.
## 코드 스타일 — Luau 문법 관례: `if-then-else`/`const` (2026-08-12 세션 신설)
**`if cond then a else b` 표현식은 Luau 공식 문법이다 — 환각/오타로
간주해 `and`/`or`로 "고치지" 말 것.** 2021년 10월 Luau에 정식 도입된
표현식 문법(공식 릴리스 노트: <https://luau.org/news/2021-10-31-luau-recap-october-2021/#if-then-else-expression>)
`cond and truthyOnly or fallback` 삼항 관용구와 달리 가운데 값이
falsy(`nil`/`false`)여도 정확하게 동작함(`bind-system-plan.md`의
`Dispatch.retractUnder` 정정 사례가 실제 버그 예시). **이 프로젝트의
기본 삼항 표현 방식은 `if-then-else`** — `cond and x or y``x`
테이블/숫자처럼 항상-truthy임이 보장될 때만 예외적으로 허용.
**`const` 바인딩도 Luau 공식 문법**(<https://luau.org/syntax/#const-bindings>)
이지만 **지금은 채택하지 않음** — 타입 추출/narrowing 등 주변 툴링이
아직 `const`를 폭넓게 지원하지 못해서, 지금 전면 도입하면 나중에 그
간극을 메꾸는 비용이 더 클 수 있음. **원칙**: 새로 짜는 코드는 일단
`local`로 — 나중 리팩터 시점에 특정 바인딩을 `const`로 바꾸는 비용이
싸 보이면 그때 바꿔도 되고, 비싸 보이면 굳이 지금 손대지 않아도 됨.
지금 `const`가 없다고 "이 프로젝트가 구식 Luau를 쓴다"고 오해하지
말 것 — 툴링 성숙도 문제일 뿐 문법 자체를 모르거나 기각한 게 아님.
## 테스트 전략: quad-base용 최소 mock (2026-08-04)
**결정**: quad-base 테스트는 Vide 선례(`initreq/vide/test/mock.luau`, 약

View file

@ -187,12 +187,13 @@ State<T | Tween<T>>`가 나옴 — Modifier/State/Source/StoreBind 코드엔
얇은 sugar — 다시 보니 매우 단순해서(사용자 표현: "생각해보니 엄청
간단하다") 다음 세션으로 미룰 이유가 없어 이번 세션에 바로 확정.
**모양**: `Tween`의 옵션(`Value` 제외 전부)을 그대로 받되, **각 필드가
`T | State<T>`를 받을 수 있음** — `Tween{...}` 자신의 필드는 plain만
받는 것과 대조적(위 "`Tween{...}`의 모든 필드는 plain 값만 받음" 절).
모순이 아님: `Animate`가 반환하는 함수 안에서 각 필드를 **`:Get()`으로
한 번 풀어 plain 값으로 만든 뒤에만** `Tween{...}`에 넘기므로, `Tween`
쪽 불변식(plain-only)은 그대로 유지됨.
**모양**: `Tween`의 옵션(`Value` 제외 전부) + `Animate` 전용 필드
`CanAnimate`(아래 절) 하나를 더해서 받되, **각 필드가 `T | State<T>`
받을 수 있음** — `Tween{...}` 자신의 필드는 plain만 받는 것과 대조적(위
"`Tween{...}`의 모든 필드는 plain 값만 받음" 절). 모순이 아님: `Animate`
반환하는 함수 안에서 각 필드를 **`:Get()`으로 한 번 풀어 plain 값으로
만든 뒤에만** `Tween{...}`에 넘기므로, `Tween` 쪽 불변식(plain-only)은
그대로 유지됨.
```lua
local function resolve(v)
@ -205,8 +206,18 @@ end
local function Animate(info)
return function(self)
local v = self:Get()
local canAnimate = resolve(info.CanAnimate)
if canAnimate == nil then
canAnimate = true -- CanAnimate 생략 시 기본 애니메이션 활성
end
if not canAnimate then
return v -- Tween로 안 감쌈 — 그대로 plain 값 반환(애니메이션 우회)
end
return Tween{
Value = self:Get(),
Value = v,
Info = resolve(info.Info),
Time = resolve(info.Time),
Style = resolve(info.Style),
@ -221,9 +232,34 @@ end
```
`resolve``and`/`or` 삼항 관용구가 아니라 `if-then-else`인 이유는
`base/bind-system-plan.md`의 2026-08-12 정정 노트와 같음 — `Override`
등 필드가 `false`일 수 있는 값이면 `isState(v) and v:Get() or v` 식은
`v:Get()`이 falsy일 때 조용히 `v`(State 객체 자신)로 새는 버그가 됨.
`base/architecture.md`의 "코드 스타일 — Luau 문법 관례" 절과 같음 —
`Override`/`CanAnimate` 등 필드가 `false`일 수 있는 값이면
`isState(v) and v:Get() or v` 식은 `v:Get()`이 falsy일 때 조용히
`v`(State 객체 자신)로 새는 버그가 됨.
**`CanAnimate: State<boolean> | boolean | nil`** — 애니메이션 자체를 켜고
끄는 필드, **생략(`nil`)이면 기본 `true`**(항상 애니메이션). `false`
resolve되면 `Tween{...}`으로 안 감싸고 `self:Get()`을 그대로 반환 —
reduceMotion류 접근성 우회가 이 필드 하나로 바로 표현됨:
```lua
-- reduceMotion: State<boolean>
Position = mySource:Compute(Animate{
Style = Enum.EasingStyle.Bounce,
CanAnimate = reduceMotion:Compute(function(r) return not r end),
})
```
이 필드도 다른 옵션들과 동일하게 값 자체가 State로 바뀌는 것만으로는
재계산을 트리거하지 않음(`Value`가 실제로 바뀌는 다음 재계산 때 그
시점의 최신 `CanAnimate`가 자연히 반영) — 위 "`Style`/`Override` 등이
State여도..." 절과 같은 이유.
**필드 이름 케이싱 메모**: 대화 중엔 `canAnimate`(소문자 시작)로
나왔으나, 같은 옵션 테이블의 나머지 필드가 전부 `Value`/`Style`/`Time`
같은 PascalCase(Roblox 프로퍼티/`Tween` opts 관례를 그대로 따름)라 여기선
`CanAnimate`로 통일 — 이 필드 하나만 다른 케이싱을 쓸 특별한 이유가
없다고 판단(확정은 아님, 다음 세션에 뒤집혀도 비용 낮음).
**왜 `:Compute`에 직접 넘길 수 있는가**: `Animate(info)`가 반환하는
`function(self) ... end``:Compute(fn)`의 콜백 시그니처(`fn(self,
@ -249,17 +285,18 @@ trailing-deps로 선언 안 됨). 그래서 `Style`이 바뀌어도 그 자체
정확히 일치하는 동작이라 별도 트리거 배선이 오히려 불필요한
복잡도였을 것.
**구 `useTween`(reduceMotion 우회) 스케치는 이 설계로 대체됨** — 이전엔
`Animate(cond, opts)`처럼 조건 인자를 받아 내부에서 plain/Tween 분기하는
전용 2-인자 시그니처를 검토했으나, 새 `Animate(info)`는 그 조건 분기를
아예 안 가짐(단일 책임 유지). 우회가 필요하면 `Animate`를 감싸는 평범한
`:Compute` 클로저로 여전히 표현 가능 — 새 프리미티브 불필요:
**구 `useTween`(reduceMotion 우회) 스케치는 `CanAnimate` 필드로 대체됨**
(위 "`CanAnimate`" 절) — 흔한 단순 토글은 그걸로 충분. 이전에 검토했던
전용 2-인자 시그니처(`Animate(cond, opts)`)는 안 씀 — `Animate(info)`
조건 인자를 별도로 안 가지는 단일 진입점 유지. `CanAnimate`로 못 담는
더 복잡한 조건(값 자체를 다른 값으로 바꿔치기하는 등)이 생기면, `Animate`
감싸는 평범한 `:Compute` 클로저로도 여전히 표현 가능 — 새 프리미티브
불필요:
```lua
-- reduceMotion: State<boolean>
Position = mySource:Compute(function(self)
if reduceMotion:Get() then
return self:Get()
if someComplexCondition() then
return someOtherValue
end
return Animate{Style = Enum.EasingStyle.Bounce}(self)
end)

View file

@ -0,0 +1,49 @@
<!-- quad-v2 세션 로그 원문 — CLAUDE.md에서 이전됨(2026-08-11 정리 세션에서 확립된 관례를 따름). -->
<!-- 이 파일은 quadnomicon 개발로그 소재용 원자료로, 당시 시행착오(정정 전 서술 포함)를 그대로 보존함. -->
<!-- 현재 유효한 설계는 이 파일이 아니라 base//research//archive/가 최종 소스 — 이 파일 안의 판단이 이후 세션에서 뒤집혔을 수 있음. -->
## 2026-08-12 세 번째 세션 — `Animate``CanAnimate` 필드 추가, Luau 문법(`if-then-else`/`const`) 공식성 문서화
**`Animate(info)`에 `CanAnimate: State<boolean> | boolean | nil` 필드
추가** — 앞선 두 세션(`2026-08-12-01`/`02`)에서 `Animate`를 확정한 직후
사용자가 "아 맞다"며 빠뜨렸던 필드를 짚음. **`nil`이면 기본 `true`**(항상
애니메이션), `false`로 resolve되면 `Tween{...}`으로 안 감싸고 `self:Get()`
그대로 반환 — reduceMotion류 접근성 우회가 이 필드 하나로 표현됨. 앞선
세션에서 "구 `useTween` 우회는 수동 `:Compute` 클로저로도 여전히
가능하다"고 남겨뒀던 예시가 사실상 `Animate` 안으로 흡수됨 — 수동 클로저는
`CanAnimate`로 못 담는 더 복잡한 조건(값 자체를 다른 값으로 바꿔치기 등)의
탈출구로만 절 문구를 축소. 필드 케이싱은 대화 중 `canAnimate`(소문자)로
나왔으나 같은 옵션 테이블의 나머지 필드가 전부 PascalCase(`Value`/`Style`/
`Time`...)라 `CanAnimate`로 정규화 — 이 필드만 다른 케이싱을 쓸 이유가
없다고 판단, 확정은 아니고 다음 세션에 뒤집혀도 비용 낮음.
**Luau `if-then-else`/`const` 문법의 공식성을 `base/architecture.md`
명문화** — 사용자가 직접 짚은 우려: 앞선 세션에서 `and`/`or` 삼항
관용구를 `if-then-else`로 고친 게(`bind-system-plan.md`의
`Dispatch.retractUnder` 정정), 나중에 이 문법을 모르는 에이전트가 "이런
문법 없음"이라며 `and`/`or`로 되돌리는 회귀를 부를 수 있음 — 실제로
`if cond then a else b`는 2021년 10월 Luau에 정식 도입된 공식 표현식
문법(사용자가 릴리스 노트 링크 직접 제공:
<https://luau.org/news/2021-10-31-luau-recap-october-2021/#if-then-else-expression>).
`architecture.md`의 "코드 스타일 — 네이밍 케이싱" 절 바로 뒤에 "코드
스타일 — Luau 문법 관례" 절을 신설해 이 사실+이 프로젝트의 기본 삼항
표현 방식이 `if-then-else`라는 것(`and`/`or`는 가운데 값이 항상-truthy로
보장될 때만 예외 허용)을 명문화. 같이 언급된 `const` 바인딩
(<https://luau.org/syntax/#const-bindings>)도 공식 문법이지만 **지금은
채택 보류** — 타입 추출/narrowing 등 주변 툴링이 아직 폭넓게 지원 못 함,
지금 전면 도입하면 나중에 그 간극을 메꾸는 비용이 더 클 수 있음. 원칙:
새 코드는 일단 `local`, 나중 리팩터 시점에 특정 바인딩을 `const`
바꾸는 비용이 싸 보이면 그때 바꾸고 비싸면 안 바꿔도 됨 — "이 프로젝트가
구식 Luau를 쓴다"는 오해 방지용으로 같이 문서화(툴링 성숙도 문제일 뿐
문법을 모르거나 기각한 게 아님).
**반영된 파일**: `research/tween-plan.md`(`Animate` 절에 `CanAnimate`
필드/예시/케이싱 메모 추가, "구 useTween 스케치" 절 축소), `base/
architecture.md`(신규 "코드 스타일 — Luau 문법 관례" 절).
**여전히 열려있는 것**: 안 바뀜 — 자연 완료(Completed) 시 per-instance
북키핑 정리 여부 하나(`research/pre-implementation-audit.md` 2-10번,
M11 착수 시).
**다음 세션이 할 일**: 안 바뀜(`ROADMAP.md` M0부터, `.claude/luau-test/`
결과 확인 우선).

View file

@ -449,3 +449,12 @@ Luau `if-then-else` 표현식을 언급하며 `.claude/base` 전역 and/or 삼
falsy-값 버그(`v`가 `false`일 때 `nil`로 새는 문제) 발견·수정, 나머지
히트는 가운데 값이 테이블/숫자라 안전 확인. `research/tween-plan.md`
이걸로 자연완료 북키핑 하나만 남기고 사실상 마감.
**2026-08-12 세 번째 세션 — `Animate``CanAnimate` 필드 추가, Luau 문법 공식성 문서화**
(`session/2026-08-12-03-cananimate-luau-syntax-note.md`)
`Animate(info)`에 빠져있던 `CanAnimate: State<boolean>|boolean|nil` 필드
추가(`nil`=기본 `true`, `false``Tween`로 안 감싸고 plain 값 그대로 —
reduceMotion류 우회가 이걸로 표현됨). `base/architecture.md`에 "코드
스타일 — Luau 문법 관례" 절 신설 — `if-then-else`(2021년 정식 도입)와
`const` 바인딩 둘 다 공식 Luau 문법임을 명문화(에이전트가 모르고
`and`/`or`로 되돌리는 회귀 방지), `const`는 툴링 미성숙으로 지금은 보류.