design: type-version-check 패키지 추출 — CheckedQuad<T, Pattern> 글롭/캐럿 확장
quad-spring-roblox류 독립 게시 플러그인엔 정확 버전 일치가 과하다는 지적에 따라, 버전 패턴 매칭(글롭 "*"/캐럿 "N^")을 quad에 종속되지 않은 범용 워크스페이스 멤버 type-version-check로 분리하고 quad-types의 CheckedQuad를 CheckedQuad<T, Pattern>으로 확장. 새 Luau 함정 2건(type function의 outer local 참조 불가, cross-package엔 export type function + 이중 꺾쇠 제네릭 인스턴스화 필요) 발견·문서화. 독립 저장소 분리는 HUMAN_TODO로 위임. Co-authored-by: qwreey <me@qwreey.moe>
This commit is contained in:
parent
297c4d459c
commit
5dfc9b9a43
21 changed files with 542 additions and 106 deletions
File diff suppressed because one or more lines are too long
|
|
@ -193,11 +193,19 @@ build`까지 실제로 돌려 링크 결과를 확인 완료(`base/project-setup
|
||||||
소스, 워크스페이스 의존성이 symlink로 연결되고 Rojo는 이를 투명하게
|
소스, 워크스페이스 의존성이 symlink로 연결되고 Rojo는 이를 투명하게
|
||||||
따라감).
|
따라감).
|
||||||
실제 구현: 루트 `pesde.toml`(`private = true`,
|
실제 구현: 루트 `pesde.toml`(`private = true`,
|
||||||
`workspace_members = ["quad-base", "quad-roblox", "quad-types"]`) +
|
`workspace_members = ["quad-base", "quad-roblox", "quad-types",
|
||||||
|
"type-version-check"]`) +
|
||||||
`quad-base/pesde.toml`/`quad-roblox/pesde.toml`/`quad-types/pesde.toml`
|
`quad-base/pesde.toml`/`quad-roblox/pesde.toml`/`quad-types/pesde.toml`
|
||||||
(각각 `[target] environment = "roblox"`), 툴체인은 `mise.toml`로 핀
|
(각각 `[target] environment = "roblox"`) + `type-version-check/pesde.toml`
|
||||||
|
(`[target] environment = "luau"`, 아래 참고), 툴체인은 `mise.toml`로 핀
|
||||||
(`rokit.toml`에서 전환, 2026-08-19 사용자 결정 — 더 범용적인 도구라는
|
(`rokit.toml`에서 전환, 2026-08-19 사용자 결정 — 더 범용적인 도구라는
|
||||||
판단, `base/project-setup-plan.md`의 "툴체인" 절 참고).
|
판단, `base/project-setup-plan.md`의 "툴체인" 절 참고). **[2026-08-19 같은
|
||||||
|
날 후속]** `type-version-check`(`[target] environment = "luau"` — quad에
|
||||||
|
종속되지 않은 범용 패키지라 다른 멤버와 달리 roblox가 아님)는 워크스페이스
|
||||||
|
네 번째 멤버로 추가됐고, `quad-types`가 이것에 workspace 의존(자기 target이
|
||||||
|
roblox라 명시적으로 `target = "luau"` 지정 필요) — `base/quad-types-plan.md`의
|
||||||
|
"`type-version-check`" 절이 소스. 사용자가 나중에 독립 저장소로 분리할
|
||||||
|
예정(`HUMAN_TODO.md` 9번).
|
||||||
**[2026-08-19 같은 날 셋째 후속 세션]** `quad-roblox`는 `quad-base`가
|
**[2026-08-19 같은 날 셋째 후속 세션]** `quad-roblox`는 `quad-base`가
|
||||||
아니라 **`quad-types`(구현 없는 타입 계약 전용 패키지)에만 workspace
|
아니라 **`quad-types`(구현 없는 타입 계약 전용 패키지)에만 workspace
|
||||||
의존** — `quad-base`는 `QuadRoblox(Quad): QuadRoblox` 패턴으로 **런타임
|
의존** — `quad-base`는 `QuadRoblox(Quad): QuadRoblox` 패턴으로 **런타임
|
||||||
|
|
@ -243,9 +251,12 @@ quad/
|
||||||
├── mise.toml # pesde/rojo/luau-lsp/selene 버전 핀
|
├── mise.toml # pesde/rojo/luau-lsp/selene 버전 핀
|
||||||
├── pesde.toml # 워크스페이스 루트(private, workspace_members)
|
├── pesde.toml # 워크스페이스 루트(private, workspace_members)
|
||||||
├── default.project.json # 루트 통합 개발/테스트용 Rojo 프로젝트
|
├── default.project.json # 루트 통합 개발/테스트용 Rojo 프로젝트
|
||||||
├── quad-types/ # 구현 없는 Quad 타입 계약 + CheckedQuad 버전체크(`base/quad-types-plan.md`)
|
├── quad-types/ # 구현 없는 Quad 타입 계약 + CheckedQuad<T,Pattern> 버전체크(`base/quad-types-plan.md`)
|
||||||
│ ├── pesde.toml
|
│ ├── pesde.toml # type_version_check workspace 의존(target="luau")
|
||||||
│ └── src/init.luau
|
│ └── src/init.luau
|
||||||
|
├── type-version-check/ # quad에 종속되지 않은 범용 버전 패턴 매칭(`base/quad-types-plan.md` "`type-version-check`" 절) — 사용자가 나중에 독립 저장소로 분리 예정(HUMAN_TODO 9번)
|
||||||
|
│ ├── pesde.toml # [target] environment = "luau"
|
||||||
|
│ └── src/init.luau # matchesPattern(런타임) + export type function CheckVersion
|
||||||
├── quad-base/
|
├── quad-base/
|
||||||
│ ├── pesde.toml
|
│ ├── pesde.toml
|
||||||
│ └── src/
|
│ └── src/
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ M3 이후 실제 구현이 아님 — `quad-base/src`는 아직 `Relate.luau`/
|
||||||
|
|
||||||
## pesde 워크스페이스 구조
|
## pesde 워크스페이스 구조
|
||||||
|
|
||||||
|
**전체 멤버 목록·최신 트리는 `architecture.md`의 "구현 착수: 소스 트리
|
||||||
|
구조 확정" 절이 소스** — 새 워크스페이스 멤버가 늘 때마다 그쪽만 갱신하면
|
||||||
|
되게 하기 위해 여기서 다시 나열/개수 세지 않는다(멤버 수가 실제로 2→3→4로
|
||||||
|
늘어난 이력에서 이 문서의 구식 트리가 갱신을 놓쳤던 게 계기). 아래는 그
|
||||||
|
구조가 실제로 동작하는 **pesde 메커니즘 자체**(문법/함정)만 다룬다 —
|
||||||
|
예시엔 최소 2-멤버 형태만 남겨두고 서술 부담을 줄임:
|
||||||
|
|
||||||
```
|
```
|
||||||
quad/
|
quad/
|
||||||
├── pesde.toml # 워크스페이스 루트, private = true, workspace_members
|
├── pesde.toml # 워크스페이스 루트, private = true, workspace_members
|
||||||
|
|
@ -37,8 +44,8 @@ quad/
|
||||||
└── selene.toml
|
└── selene.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
- **루트 `pesde.toml`**: `private = true`(게시 안 됨) + `workspace_members =
|
- **루트 `pesde.toml`**: `private = true`(게시 안 됨) + `workspace_members`
|
||||||
["quad-base", "quad-roblox"]`. `[target] environment = "roblox"`도
|
(멤버 목록은 `architecture.md`가 소스). `[target] environment = "roblox"`도
|
||||||
필요(공식 workspace 가이드 예제가 루트에도 `[target]`을 요구함 — 이
|
필요(공식 workspace 가이드 예제가 루트에도 `[target]`을 요구함 — 이
|
||||||
세션엔 `roblox` 하나만 있어 실제로 검증 안 됨, 필요 여부/의미는 M5 이후
|
세션엔 `roblox` 하나만 있어 실제로 검증 안 됨, 필요 여부/의미는 M5 이후
|
||||||
재확인 후보).
|
재확인 후보).
|
||||||
|
|
@ -59,9 +66,18 @@ quad/
|
||||||
"package not found"로 실패한다. `[dependencies]`의 `workspace = "scope/name"`
|
"package not found"로 실패한다. `[dependencies]`의 `workspace = "scope/name"`
|
||||||
줄은 **직접 손으로 쓸 것**(위 표기 그대로 — 실제로 `pesde install`이
|
줄은 **직접 손으로 쓸 것**(위 표기 그대로 — 실제로 `pesde install`이
|
||||||
받아들이는 걸 확인함).
|
받아들이는 걸 확인함).
|
||||||
- **`pesde install`은 워크스페이스 루트에서 한 번**만 돌리면 전체가
|
- **`pesde install`은 워크스페이스 루트에서 한 번**만 돌리면 **모든
|
||||||
갱신됨(`qwreey/quad`/`qwreey/quad_base`/`qwreey/quad_roblox` 셋 다
|
워크스페이스 멤버**가 스캔·링크됨(개수는 `architecture.md`의
|
||||||
스캔·링크).
|
`workspace_members`가 소스 — 새 멤버가 늘어도 이 동작은 안 바뀜).
|
||||||
|
- **[2026-08-19 후속, `type-version-check` 신설 때 실측] 의존하는
|
||||||
|
워크스페이스 멤버의 `target`이 자기 자신의 기본 target과 다르면
|
||||||
|
`workspace = "..."` 의존 선언에 `target = "..."`를 명시해야 한다.**
|
||||||
|
`quad-types`(기본 target `roblox`)가 `type-version-check`(자기
|
||||||
|
`[target] environment = "luau"`)에 의존할 때, `target` 없이 `{
|
||||||
|
workspace = "qwreey/type_version_check", version = "^" }`만 쓰면
|
||||||
|
`pesde install`이 `no workspace member found with name
|
||||||
|
qwreey/type_version_check and target roblox`로 실패한다 — `target =
|
||||||
|
"luau"`를 추가해야 해소됨.
|
||||||
|
|
||||||
## 툴체인 — `rokit.toml`에서 `mise.toml`로 전환 (2026-08-19)
|
## 툴체인 — `rokit.toml`에서 `mise.toml`로 전환 (2026-08-19)
|
||||||
|
|
||||||
|
|
@ -231,6 +247,22 @@ Rojo/Studio가 실제로 소비하는 게 그 경로이고 위에서 확인했
|
||||||
`.luaurc` symlink opt-in 토글이 미래에 생기면 이 절 전체가 불필요해짐 —
|
`.luaurc` symlink opt-in 토글이 미래에 생기면 이 절 전체가 불필요해짐 —
|
||||||
그때 다시 볼 것).
|
그때 다시 볼 것).
|
||||||
|
|
||||||
|
**[2026-08-19 같은 날 넷째 후속 세션] 의존 대상의 `target`에 따라 링크
|
||||||
|
디렉토리 이름이 달라진다** — `quad-types`(target `roblox`)가
|
||||||
|
`type-version-check`(target `luau`)에 의존하면, `quad-base`/`quad-roblox`가
|
||||||
|
쓰는 `roblox_packages/`와 달리 `quad-types/src/init.luau`는
|
||||||
|
`require("./luau_packages/type_version_check")`로 **`luau_packages/`**
|
||||||
|
아래에서 링크를 찾는다 — pesde가 의존 대상 패키지 자신의 target 이름으로
|
||||||
|
디렉토리를 분리하기 때문(`.gitignore`에 `luau_packages/`도 이미
|
||||||
|
포함돼 있어 별도 조치 불필요).
|
||||||
|
|
||||||
|
같은 워크어라운드가 2단 의존 체인에도 그대로 재적용됨 — `type-version-check` 신설로
|
||||||
|
`quad-types`→`type-version-check`가 추가되면서 `quad-base`/`quad-roblox`→
|
||||||
|
`quad-types`→`type-version-check`처럼 깊이 2인 워크스페이스 의존 그래프가
|
||||||
|
생겼는데, `pesde install` 후 같은 심볼릭 링크 치환을 반복 적용하는 것만으로
|
||||||
|
`luau-analyze`/`luau` 양쪽 다 문제없이 동작 확인됨 — 이 우회가 단일
|
||||||
|
깊이에 국한되지 않고 일반화됨이 실측으로 재확인됨.
|
||||||
|
|
||||||
## `.luaurc` — alias는 여전히 편집기 전용
|
## `.luaurc` — alias는 여전히 편집기 전용
|
||||||
|
|
||||||
`.luaurc`의 `aliases`(`@quad-base`/`@quad-roblox`)는 **런타임
|
`.luaurc`의 `aliases`(`@quad-base`/`@quad-roblox`)는 **런타임
|
||||||
|
|
@ -275,8 +307,10 @@ require에서 여전히 안 먹는다** — `architecture.md`가 이미 이렇
|
||||||
**[2026-08-19 실측, 최초 서술 정정]** 처음엔 "워크스페이스 루트에 딱
|
**[2026-08-19 실측, 최초 서술 정정]** 처음엔 "워크스페이스 루트에 딱
|
||||||
하나만 생긴다"고 적었으나 **틀렸음** — 실제로는 `pesde install`이
|
하나만 생긴다"고 적었으나 **틀렸음** — 실제로는 `pesde install`이
|
||||||
**워크스페이스 멤버마다 각자의 `pesde.lock`도 같이 만든다**(루트
|
**워크스페이스 멤버마다 각자의 `pesde.lock`도 같이 만든다**(루트
|
||||||
`pesde.lock` 1개 + `quad-base/pesde.lock` + `quad-roblox/pesde.lock`,
|
`pesde.lock` 1개 + 멤버마다 1개씩, 개수는 `architecture.md`의
|
||||||
총 3개). 루트 것은 `[workspace."qwreey/quad_base"]`류 멤버 매핑만
|
`workspace_members`를 따라간다 — 2026-08-19 이 세션 안에서만 2개
|
||||||
|
멤버(2+1개 lock)에서 4개 멤버(4+1개 lock)로 늘어난 전례가 있어 여기 숫자를
|
||||||
|
고정하지 않는다). 루트 것은 `[workspace."qwreey/quad_base"]`류 멤버 매핑만
|
||||||
담고, 멤버 것들은 각자의 실제 의존성 그래프를 담는다(`quad_roblox`의
|
담고, 멤버 것들은 각자의 실제 의존성 그래프를 담는다(`quad_roblox`의
|
||||||
lock엔 `[graph."qwreey/quad_base@0.0.0 roblox"]` + `pkg_ref.ref_ty =
|
lock엔 `[graph."qwreey/quad_base@0.0.0 roblox"]` + `pkg_ref.ref_ty =
|
||||||
"workspace"`가 있음, `quad_base`는 의존성이 없어 메타데이터만).
|
"workspace"`가 있음, `quad_base`는 의존성이 없어 메타데이터만).
|
||||||
|
|
@ -295,7 +329,10 @@ lockfile들이 **게시되는 대상이 아니기** 때문 — 루트는 `privat
|
||||||
## 확인 완료 / 아직 확인 안 된 것
|
## 확인 완료 / 아직 확인 안 된 것
|
||||||
|
|
||||||
**확인 완료(이 세션, 실제 pesde/luau 실행 근거)**:
|
**확인 완료(이 세션, 실제 pesde/luau 실행 근거)**:
|
||||||
- pesde 워크스페이스 설치가 3개 패키지(루트+2서브) 전부에 대해 성공
|
- pesde 워크스페이스 설치가 당시 워크스페이스 멤버 전부(그때는
|
||||||
|
루트+2서브)에 대해 성공(**[2026-08-19 후속]** 이후 `quad-types`/
|
||||||
|
`type-version-check` 추가로 멤버가 늘어난 뒤에도 같은 절차로 계속 성공 —
|
||||||
|
아래 후속 문단들 참고)
|
||||||
- 패키지 이름 문자 제약(하이픈 금지)
|
- 패키지 이름 문자 제약(하이픈 금지)
|
||||||
- `workspace = "scope/name"` 의존성 선언 문법
|
- `workspace = "scope/name"` 의존성 선언 문법
|
||||||
- `@self`가 `init.luau`의 형제 파일 접근에 필수라는 것(런타임+
|
- `@self`가 `init.luau`의 형제 파일 접근에 필수라는 것(런타임+
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
**상태**: base — 2026-08-19 세션에 신설·구현·검증까지 완료. 워크스페이스
|
**상태**: base — 2026-08-19 세션에 신설·구현·검증까지 완료. 워크스페이스
|
||||||
세 번째 멤버 `quad-types`의 존재 이유, `AddPlugin`/`CheckedQuad`의 정확한
|
세 번째 멤버 `quad-types`의 존재 이유, `AddPlugin`/`CheckedQuad`의 정확한
|
||||||
사용법, 그 배선에서 실제로 깨졌던 Luau 함정들을 정리.
|
사용법, 그 배선에서 실제로 깨졌던 Luau 함정들을 정리. **[같은 날 후속]**
|
||||||
|
버전 패턴 매칭 자체는 quad에 종속되지 않은 범용 패키지
|
||||||
|
`type-version-check`(워크스페이스 네 번째 멤버)로 분리됐고, `CheckedQuad<T>`는
|
||||||
|
`CheckedQuad<T, Pattern>`으로 확장돼 그 위에 얹힌다 — 아래 "`type-version-check`"
|
||||||
|
절.
|
||||||
|
|
||||||
## 왜 필요한가 — dev-dependency로는 못 푸는 문제
|
## 왜 필요한가 — dev-dependency로는 못 푸는 문제
|
||||||
|
|
||||||
|
|
@ -44,8 +48,12 @@ pesde의 워크스페이스 의존성은 **패키지 단위**로만 걸린다
|
||||||
|
|
||||||
```
|
```
|
||||||
quad-types/
|
quad-types/
|
||||||
├── pesde.toml # name = "qwreey/quad_types"
|
├── pesde.toml # name = "qwreey/quad_types", type_version_check workspace 의존
|
||||||
└── src/init.luau # export type Quad, type function CheckVersion, export type CheckedQuad<T>
|
└── src/init.luau # export type Quad, export type CheckedQuad<T, Pattern>
|
||||||
|
|
||||||
|
type-version-check/ # 워크스페이스 네 번째 멤버, quad에 종속되지 않음
|
||||||
|
├── pesde.toml # name = "qwreey/type_version_check", environment = "luau"
|
||||||
|
└── src/init.luau # matchesPattern(런타임), export type function CheckVersion
|
||||||
```
|
```
|
||||||
|
|
||||||
- `quad-base`는 `quad_types`에 workspace 의존 — **자기 `Quad` 타입을
|
- `quad-base`는 `quad_types`에 workspace 의존 — **자기 `Quad` 타입을
|
||||||
|
|
@ -99,23 +107,73 @@ AddPlugin: <Self, P>(self: Self, pluginFn: (Self) -> P) -> Self & P
|
||||||
identity에 의존하므로(`base/module-lifecycle-plan.md`의 "New()의 내부 구성" 절),
|
identity에 의존하므로(`base/module-lifecycle-plan.md`의 "New()의 내부 구성" 절),
|
||||||
`AddPlugin`이 새 테이블을 반환하면 그 추적이 끊긴다.
|
`AddPlugin`이 새 테이블을 반환하면 그 추적이 끊긴다.
|
||||||
|
|
||||||
## `CheckedQuad<T>` — 버전 불일치를 컴파일 타임에 사람이 읽을 메시지로
|
## `type-version-check` — 범용 버전 패턴 매칭 패키지
|
||||||
|
|
||||||
|
**[2026-08-19 신설]** 처음엔 `CheckVersion<T>`가 정확 일치(`"0.0.0"`)만
|
||||||
|
보는 quad-types 내부 함수였다. 그런데 정확 일치는 `quad-spring`/
|
||||||
|
`quad-spring-roblox`처럼 **독립적으로 게시되는 백엔드 플러그인** 쌍엔 너무
|
||||||
|
빡빡하다 — 최신 `quad-spring-roblox`가 예전 `quad-spring`도 잘 다루는
|
||||||
|
경우가 흔할 텐데, 정확 일치를 강제하면 그때마다 재게시가 필요해진다
|
||||||
|
(**사용자 판단**: "구현해주는것 정말 쉽고... 있으면 좋다고 생각함").
|
||||||
|
그래서 글롭/캐럿 패턴을 지원하는 별도 패키지로 뺐다 — quad 전용 이름을
|
||||||
|
안 섞어서 quad-spring류가 quad-base 전체를 끌고 올 필요 없이 이것만
|
||||||
|
가볍게 의존하게 하기 위함이기도 하다.
|
||||||
|
|
||||||
|
**[2026-08-19] 지금은 quad 모노레포 워크스페이스의 네 번째 멤버로 두지만,
|
||||||
|
사용자가 나중에 독립 저장소로 직접 분리할 예정** — `HUMAN_TODO.md` 참고.
|
||||||
|
|
||||||
|
**패턴 문법**(`.`로 나뉜 각 자리): `"*"` = 와일드카드, `"N^"` = 그 자리
|
||||||
|
숫자값이 N **이상**이면 통과(caret), 그 외 = 정확히 같은 문자열이어야
|
||||||
|
통과. 예: `"3.*.*"`(메이저만 고정), `"3.3^.4^"`(마이너 3 이상 + 패치
|
||||||
|
4 이상), `"0.0.0"`(정확 일치 — quad-types가 지금 쓰는 패턴).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
export type function CheckVersion(actual: type, pattern: type): type
|
||||||
|
```
|
||||||
|
|
||||||
|
`actual`/`pattern` 둘 다 문자열 리터럴(singleton) 타입이어야 하고, 일치하면
|
||||||
|
트리비얼한 `true`(`types.singleton(true)`) 하나만 반환 — `quad-types`
|
||||||
|
"함정 3"과 같은 이유로 원본 타입을 절대 반환하지 않는다.
|
||||||
|
|
||||||
|
**Luau 신규 실측 함정 2건**(이 세션에 처음 발견, `typing-limits.md`가
|
||||||
|
다루는 "타입 시스템 해석 한계"와는 결이 달라 여기 기록):
|
||||||
|
- **`type function`은 같은 파일의 바깥 스코프 로컬 함수를 아예 참조 못
|
||||||
|
한다** — `Type function cannot reference outer local 'X'`로 컴파일
|
||||||
|
자체가 실패. 그래서 런타임용 `matchesPattern`과 `CheckVersion` 내부의
|
||||||
|
매칭 로직은 **물리적으로 별개 함수로 중복**돼 있다
|
||||||
|
(`type-version-check/src/init.luau`) — 하나를 고치면 반드시 다른
|
||||||
|
하나도 같이 고칠 것.
|
||||||
|
- **cross-package 사용엔 `export type function`이 필요**하다(`type
|
||||||
|
function`만으론 안 됨) — 안 그러면 다른 파일에서 `Unknown type
|
||||||
|
'Module.CheckVersion'`으로 막힌다. 그리고 명시적 제네릭 인스턴스화가
|
||||||
|
**2개 이상**이면 단일 꺾쇠(`Foo<A, B>`)가 비교 연산자로 오파싱되니
|
||||||
|
반드시 이중 꺾쇠(`Foo<<A, B>>`)를 써야 한다(코퍼스에 이미 있던
|
||||||
|
`AttributeKey<<T>>` 관례와 같은 이유).
|
||||||
|
|
||||||
|
`Version` 필드는 Luau 내장 `index<T, "Version">` type function으로 뽑는다
|
||||||
|
(수동 `t:readproperty(...)`보다 간결 — **사용자 제안**으로 채택, 실측 확인
|
||||||
|
완료).
|
||||||
|
|
||||||
|
## `CheckedQuad<T, Pattern>` — 버전 불일치를 컴파일 타임에 사람이 읽을 메시지로
|
||||||
|
|
||||||
**왜 필요한가**: `quad-roblox`가 `quad_base`를 pesde `[dependencies]`로
|
**왜 필요한가**: `quad-roblox`가 `quad_base`를 pesde `[dependencies]`로
|
||||||
선언하지 않고 런타임 주입으로만 받게 되면서, **pesde 자신의 semver 충돌
|
선언하지 않고 런타임 주입으로만 받게 되면서, **pesde 자신의 semver 충돌
|
||||||
방지 장치가 이 관계엔 전혀 안 걸린다** — 선언된 의존성이 아니라 그냥
|
방지 장치가 이 관계엔 전혀 안 걸린다** — 선언된 의존성이 아니라 그냥
|
||||||
함수 인자라서. `CheckedQuad<T>`가 그 빈자리를 메꾸는 컴파일 타임
|
함수 인자라서. `CheckedQuad<T, Pattern>`이 그 빈자리를 메꾸는 컴파일 타임
|
||||||
대체 안전장치다(사용자 판단: "런타임 에러까지 내려면 quad-roblox
|
대체 안전장치다(사용자 판단: "런타임 에러까지 내려면 quad-roblox
|
||||||
소스에 버전을 하드코딩해야 하는데 그건 과함 — 타입 에러만 내는 걸로
|
소스에 버전을 하드코딩해야 하는데 그건 과함 — 타입 에러만 내는 걸로
|
||||||
충분").
|
충분"). `Pattern`은 위 `type-version-check`의 글롭/캐럿 패턴 문자열 —
|
||||||
|
quad-base/quad-roblox처럼 같은 모노레포에서 항상 같이 개발되는 관계는
|
||||||
|
정확 일치(`"0.0.0"`)를, quad-spring-roblox류 독립 게시 플러그인은
|
||||||
|
`"0.*.*"` 같은 느슨한 패턴을 직접 골라 쓴다.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
export type CheckedQuad<T> = T & { __versionCheck: CheckVersion<T> }
|
export type CheckedQuad<T, Pattern> = T & { __versionCheck: TypeVersionCheck.CheckVersion<index<T, "Version">, Pattern> }
|
||||||
```
|
```
|
||||||
|
|
||||||
**사용법**:
|
**사용법**:
|
||||||
```lua
|
```lua
|
||||||
local function CheckQuad<T>(quad: T): QuadTypes.CheckedQuad<T>
|
local function CheckQuad<T>(quad: T): QuadTypes.CheckedQuad<T, "0.0.0">
|
||||||
return quad :: any
|
return quad :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -141,8 +199,8 @@ local function QuadRoblox<T>(quad: T): T
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
체크는 **리턴 타입/필드 타입처럼 호출부마다 실제로 해석되는 자리**에
|
체크는 **리턴 타입/필드 타입처럼 호출부마다 실제로 해석되는 자리**에
|
||||||
박아 넣어야 한다. 이게 `CheckedQuad<T>`가 함수 파라미터/반환 타입
|
박아 넣어야 한다. 이게 `CheckedQuad<T, Pattern>`이 함수 파라미터/반환
|
||||||
표현식 안에 직접 나타나야 하는 이유고, `__versionCheck` 필드도 **실제로
|
타입 표현식 안에 직접 나타나야 하는 이유고, `__versionCheck` 필드도 **실제로
|
||||||
참조해야만** 평가된다(lazy) — 위 사용법 예제의 `local _ =
|
참조해야만** 평가된다(lazy) — 위 사용법 예제의 `local _ =
|
||||||
checked.__versionCheck` 줄이 빠지면 검사가 조용히 스킵된다.
|
checked.__versionCheck` 줄이 빠지면 검사가 조용히 스킵된다.
|
||||||
|
|
||||||
|
|
@ -174,20 +232,30 @@ function`을 거치면 이후 제네릭 self 메소드 체이닝이 조용히
|
||||||
## 실측 근거
|
## 실측 근거
|
||||||
|
|
||||||
`.claude/luau-test/done/23-type-quadtypes-checkversion-addplugin.luau` —
|
`.claude/luau-test/done/23-type-quadtypes-checkversion-addplugin.luau` —
|
||||||
실제 `quad-types`/`quad-base`를 `require`해서 위 사용법 그대로
|
실제 `quad-types`/`quad-base`/`type-version-check`를 `require`해서 위
|
||||||
재현: 양성 경로(버전 일치 + `AddPlugin` 2회 체이닝 + 이전 확장 필드
|
사용법 그대로 재현: 양성 경로(버전 일치 + `AddPlugin` 2회 체이닝 + 이전
|
||||||
유지) 전부 클린, 음성 경로(버전 불일치)는 정확히 그 줄에서
|
확장 필드 유지) 전부 클린, 음성 경로(버전 불일치)는 정확히 그 줄에서
|
||||||
`TypeError: quad-base version mismatch: ...` 하나만.
|
`TypeError: type-version-check: version "9.9.9" does not match pattern
|
||||||
|
"0.0.0"` 하나만.
|
||||||
|
|
||||||
`quad-base/test/smoke.plugin.luau` — 실제 런타임 `AddPlugin` 구현(mutate
|
`quad-base/test/smoke.plugin.luau` — 실제 런타임 `AddPlugin` 구현(mutate
|
||||||
+ identity 보존 + 체이닝)을 실행 레벨로 검증.
|
+ identity 보존 + 체이닝)을 실행 레벨로 검증.
|
||||||
|
|
||||||
## 남은 것
|
## 남은 것
|
||||||
|
|
||||||
- `quad-roblox`가 실제로 `CheckedQuad<T>`를 쓰는 진입점(`QuadRoblox` 등)
|
- `quad-roblox`가 실제로 `CheckedQuad<T, Pattern>`을 쓰는 진입점
|
||||||
구현은 M5 — 지금은 quad-roblox/src가 비어 있어 이 문서의 사용법
|
(`QuadRoblox` 등) 구현은 M5 — 지금은 quad-roblox/src가 비어 있어 이
|
||||||
예제가 실제 위치는 아직 없음.
|
문서의 사용법 예제가 실제 위치는 아직 없음.
|
||||||
- `_initializedBy`(별도 문자열 마커, backend 유일 슬롯 가드)와의 관계는
|
- `_initializedBy`(별도 문자열 마커, backend 유일 슬롯 가드)와의 관계는
|
||||||
`base/module-lifecycle-plan.md`의 "New()의 내부 구성" 절 참고 — `CheckedQuad`는
|
`base/module-lifecycle-plan.md`의 "New()의 내부 구성" 절 참고 — `CheckedQuad`는
|
||||||
**버전** 호환성만 보고, **누가 이미 backend를 설치했는지**는 별개
|
**버전** 호환성만 보고, **누가 이미 backend를 설치했는지**는 별개
|
||||||
문제로 계속 `_initializedBy`가 담당한다.
|
문제로 계속 `_initializedBy`가 담당한다.
|
||||||
|
- **[백로그, 2026-08-19 신설]** `quad-roblox-types`(가칭) — `quad-types`와
|
||||||
|
같은 패턴으로, `quad-roblox` 전체 대신 그 타입만 필요한 모듈을 위한
|
||||||
|
패키지. **사용자가 지금 만들 필요는 없다고 명시적으로 후순위 지정** —
|
||||||
|
다만 이후 쉽게 뽑을 수 있게 `quad-roblox`의 공개 타입은 지금부터 단일
|
||||||
|
`src/init.luau`(또는 `types.luau`) 형태로 몰아두는 걸 관례로 유지할 것
|
||||||
|
(quad-types 자신이 이미 이 형태 — `Quad`/`CheckedQuad` 둘 다
|
||||||
|
`src/init.luau` 하나에 있음).
|
||||||
|
- **[HUMAN_TODO]** `type-version-check`는 사용자가 나중에 독립 저장소로
|
||||||
|
직접 분리할 예정 — 루트 `HUMAN_TODO.md` 참고.
|
||||||
|
|
|
||||||
|
|
@ -399,26 +399,29 @@ local extended = checked:AddPlugin(somePlugin) -- 여기서 깨짐:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
-- ✅ 원본 T는 type function을 한 번도 안 거침
|
-- ✅ 원본 T는 type function을 한 번도 안 거침
|
||||||
type CheckedQuad<T> = T & { __versionCheck: CheckVersion<T> }
|
type CheckedQuad<T, Pattern> = T & { __versionCheck: CheckVersion<T, Pattern> }
|
||||||
|
|
||||||
local checked: CheckedQuad<Quad> = ...
|
local checked: CheckedQuad<Quad, "0.0.0"> = ...
|
||||||
local _ = checked.__versionCheck -- 강제 평가(아래 캐비엇 참고)
|
local _ = checked.__versionCheck -- 강제 평가(아래 캐비엇 참고)
|
||||||
local extended = checked:AddPlugin(somePlugin) -- 안 깨짐 — checked의 T 부분은 순수함
|
local extended = checked:AddPlugin(somePlugin) -- 안 깨짐 — checked의 T 부분은 순수함
|
||||||
```
|
```
|
||||||
|
|
||||||
`quad-types-plan.md`의 "`CheckedQuad<T>`" 절에 전체 배선과 실측 과정이
|
`quad-types-plan.md`의 "`CheckedQuad<T, Pattern>`" 절에 전체 배선과 실측
|
||||||
있습니다 — ①`error()` 대신 `print`+`types.never`, ②검증은 함수 본문
|
과정이 있습니다 — ①`error()` 대신 `print`+`types.never`, ②검증은 함수
|
||||||
로컬 타입 별칭이 아니라 리턴/필드 타입 표현식 자체에 박아 넣어야
|
본문 로컬 타입 별칭이 아니라 리턴/필드 타입 표현식 자체에 박아 넣어야
|
||||||
호출부마다 재평가됨, ③(이 항목) 원본을 절대 반환하지 않고 별도 필드로
|
호출부마다 재평가됨, ③(이 항목) 원본을 절대 반환하지 않고 별도 필드로
|
||||||
격리, 세 가지가 함께 필요합니다.
|
격리, 세 가지가 함께 필요합니다. **[2026-08-19 후속]** 실제 버전 매칭
|
||||||
|
로직(`CheckVersion`)은 quad에 종속되지 않은 별도 패키지
|
||||||
|
`type-version-check`로 분리됐지만, 이 항목이 다루는 "패스스루도 이력만으로
|
||||||
|
오염된다" 함정과 그 회피(별도 가상 필드 격리)는 그대로 유효합니다.
|
||||||
|
|
||||||
### 언제 마주치는가
|
### 언제 마주치는가
|
||||||
|
|
||||||
`AddPlugin`처럼 **제네릭 self 파라미터**를 쓰는 메소드가 있는 타입에,
|
`AddPlugin`처럼 **제네릭 self 파라미터**를 쓰는 메소드가 있는 타입에,
|
||||||
`type function` 기반 검사/변형을 적용하려는 모든 자리 — quad에서는
|
`type function` 기반 검사/변형을 적용하려는 모든 자리 — quad에서는
|
||||||
지금 `quad-types`의 `CheckedQuad<T>`가 유일한 실사례지만, 앞으로 비슷한
|
지금 `quad-types`의 `CheckedQuad<T, Pattern>`이 유일한 실사례지만, 앞으로
|
||||||
"타입 레벨 게이트 + 체이닝 가능한 API" 조합을 설계할 때마다 재발할 수
|
비슷한 "타입 레벨 게이트 + 체이닝 가능한 API" 조합을 설계할 때마다 재발할
|
||||||
있는 일반 패턴입니다. 아래 §8 체크리스트에 항목 추가.
|
수 있는 일반 패턴입니다. 아래 §8 체크리스트에 항목 추가.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
|
||||||
| `20-slot-splice-index-arithmetic.luau` | **[2026-08-13 신규]** `Slot:Splice(index, removeCount, ...newElements)`의 shift+recompute 1회 계산이, `Extract`/`Add` 반복으로 재현한 참조 구현과 항상 같은 결과를 내는지 — 제거/삽입 길이가 다를 때(delta 양수/음수) 뒤 요소가 밀리는 방향과 양을 헷갈리는 off-by-one 위험(이 프로젝트가 `Dispatch.recompute`에서 실제로 냈던 것과 같은 클래스의 버그)을 경계값 케이스로 검증 | `slot-plan.md` "확정" CRUD 표 + "`Splice` 신설" 절(2026-08-12 열다섯 번째 세션), `dispatch-core-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) |
|
| `20-slot-splice-index-arithmetic.luau` | **[2026-08-13 신규]** `Slot:Splice(index, removeCount, ...newElements)`의 shift+recompute 1회 계산이, `Extract`/`Add` 반복으로 재현한 참조 구현과 항상 같은 결과를 내는지 — 제거/삽입 길이가 다를 때(delta 양수/음수) 뒤 요소가 밀리는 방향과 양을 헷갈리는 off-by-one 위험(이 프로젝트가 `Dispatch.recompute`에서 실제로 냈던 것과 같은 클래스의 버그)을 경계값 케이스로 검증 | `slot-plan.md` "확정" CRUD 표 + "`Splice` 신설" 절(2026-08-12 열다섯 번째 세션), `dispatch-core-plan.md`의 `recompute` off-by-one 수정 사례(2026-08-11 여섯 번째 세션) |
|
||||||
| `21-type-store-undeclared-key-rejected.luau` (타입체크 전용) | **[2026-08-19 신규]** `Store<{field: T}>`로 선언 안 된 이름에 dot-access하면 `type function`이 합성한 결과 타입(`ProcessStoreType`, `16`과 동일)에 그 프로퍼티가 없어 타입 시간에 거부되는지 — `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측. 통과: 미선언 키 접근 2건이 정확히 `TypeError`로 걸림 | `store-plan.md` "Store = Source들의 이름 붙은 모음" 절의 "[확인 요구, 2026-08-18 구현 전 QA]" 항목, `todos.md` 00번 |
|
| `21-type-store-undeclared-key-rejected.luau` (타입체크 전용) | **[2026-08-19 신규]** `Store<{field: T}>`로 선언 안 된 이름에 dot-access하면 `type function`이 합성한 결과 타입(`ProcessStoreType`, `16`과 동일)에 그 프로퍼티가 없어 타입 시간에 거부되는지 — `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측. 통과: 미선언 키 접근 2건이 정확히 `TypeError`로 걸림 | `store-plan.md` "Store = Source들의 이름 붙은 모음" 절의 "[확인 요구, 2026-08-18 구현 전 QA]" 항목, `todos.md` 00번 |
|
||||||
| `22-runtime-ref-preref-postref-brand.luau` | **[2026-08-19 신규]** 구 `13`의 런타임(B) 절반을 분리한 것 — `isPreRef`/`isPostRef`가 같은 층위의 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)인지, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지 | `ref-plan.md`의 "`PostRef`" 절, `brand-plan.md`의 `Brand` 절 |
|
| `22-runtime-ref-preref-postref-brand.luau` | **[2026-08-19 신규]** 구 `13`의 런타임(B) 절반을 분리한 것 — `isPreRef`/`isPostRef`가 같은 층위의 배타적 형제(둘 다 `isRef`엔 `true`, 서로에겐 `false`)인지, Leaf 핸들러 흉내(`isRef(v) and not isPreRef(v) and not isPostRef(v)`)가 Ref/PreRef/PostRef 셋을 정확히 갈라내는지 | `ref-plan.md`의 "`PostRef`" 절, `brand-plan.md`의 `Brand` 절 |
|
||||||
| `23-type-quadtypes-checkversion-addplugin.luau` (타입체크 전용) | **[2026-08-19 신규]** 실제 `quad-types`/`quad-base`를 `require`해서 `CheckedQuad<T>`(버전 체크)가 `AddPlugin<Self,P>` 체이닝과 맞물려 동작하는지 — 양성(버전 일치 + 2단 체이닝 + 이전 확장 필드 보존), 음성(버전 불일치 → 강제 참조 시점에 정확히 `TypeError`). `type function`을 거친 값은 패스스루라도 이후 제네릭 self 체이닝이 깨진다는 걸 이 스파이크가 재작성 과정에서 직접 발견 | `quad-types-plan.md`, `typing-limits.md` §6 |
|
| `23-type-quadtypes-checkversion-addplugin.luau` (타입체크 전용) | **[2026-08-19 신규, 같은 날 후속으로 재작성]** 실제 `quad-types`/`quad-base`/`type-version-check`를 `require`해서 `CheckedQuad<T, Pattern>`(글롭/캐럿 버전 패턴 체크, `type-version-check` 위에 얹힘)이 `AddPlugin<Self,P>` 체이닝과 맞물려 동작하는지 — 양성(버전 일치 + 2단 체이닝 + 이전 확장 필드 보존), 음성(버전 불일치 → 강제 참조 시점에 정확히 `TypeError`). `type function`을 거친 값은 패스스루라도 이후 제네릭 self 체이닝이 깨진다는 걸 이 스파이크가 재작성 과정에서 직접 발견. 재작성 과정에서 `export type function`(cross-package 필수)과 2개 이상 명시 제네릭 인스턴스화의 이중 꺾쇠(`Foo<<A,B>>`) 요구도 추가로 실측 확인 | `quad-types-plan.md`, `typing-limits.md` §6 |
|
||||||
|
|
||||||
## 공통 유틸리티
|
## 공통 유틸리티
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
# 스파이크 상태판 — **폴더가 곧 상태**
|
# 스파이크 상태판 — **폴더가 곧 상태**
|
||||||
|
|
||||||
> 마지막 갱신: 2026-08-19 — 신규 `quad-types` 패키지(`CheckedQuad<T>`
|
> 마지막 갱신: 2026-08-19 — 신규 `quad-types` 패키지(`CheckedQuad<T, Pattern>`
|
||||||
> 버전 체크 + `AddPlugin<Self,P>` 체이닝) 검증용 `23` 신규 추가 → `done/`
|
> 버전 패턴 체크 + `AddPlugin<Self,P>` 체이닝) 검증용 `23` 신규 추가 →
|
||||||
> 직행. 그 과정에서 `type function`을 거친 값은 패스스루라도 이후
|
> `done/` 직행, 같은 날 후속으로 `type-version-check` 분리에 맞춰 재작성.
|
||||||
|
> 그 과정에서 `type function`을 거친 값은 패스스루라도 이후
|
||||||
> 제네릭 self 체이닝이 깨진다는 새 Luau 함정을 발견(`typing-limits.md`
|
> 제네릭 self 체이닝이 깨진다는 새 Luau 함정을 발견(`typing-limits.md`
|
||||||
> §6로 승격). 직전 갱신은 같은 날 — `13`을 타입 전용/런타임 두 파일로 분리
|
> §6로 승격). 직전 갱신은 같은 날 — `13`을 타입 전용/런타임 두 파일로 분리
|
||||||
> (A의 더미 스텁이 B 실행을 막던 문제 해결) + PostRef까지 확장, 런타임
|
> (A의 더미 스텁이 B 실행을 막던 문제 해결) + PostRef까지 확장, 런타임
|
||||||
|
|
@ -124,7 +125,7 @@
|
||||||
| `14-type-nilable-default-overload` | ⚠️ 부분 — 의도한 오용은 막지만 정상 nilable 사용례까지 막아 현 스케치로는 채택 불가. **설계 결정은 아직 필요 없음**(대안이 이미 UB 경고로 존재)이라 `review-required`가 아님 |
|
| `14-type-nilable-default-overload` | ⚠️ 부분 — 의도한 오용은 막지만 정상 nilable 사용례까지 막아 현 스케치로는 채택 불가. **설계 결정은 아직 필요 없음**(대안이 이미 UB 경고로 존재)이라 `review-required`가 아님 |
|
||||||
| `16-type-store-key-typefunction` | **[2026-08-15]** ✅ 통과 — 원인은 설계 문제가 아니라 `types.newfunction` API 버전 드리프트(배열이 아니라 `{head=...}` 레코드). `ProcessStoreType<Input>`이 정확히 `{ty: Source<string>, count: Source<number>}` 구조를 만족, 음성 대조군 4건(틀린 Get/Set 타입 2건, 존재하지 않는 메소드) 전부 정확히 에러. 근거: `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절 |
|
| `16-type-store-key-typefunction` | **[2026-08-15]** ✅ 통과 — 원인은 설계 문제가 아니라 `types.newfunction` API 버전 드리프트(배열이 아니라 `{head=...}` 레코드). `ProcessStoreType<Input>`이 정확히 `{ty: Source<string>, count: Source<number>}` 구조를 만족, 음성 대조군 4건(틀린 Get/Set 타입 2건, 존재하지 않는 메소드) 전부 정확히 에러. 근거: `audit/type-recursive-issue-with-typeof/REPORT.md` 6-1절 |
|
||||||
| `21-type-store-undeclared-key-rejected` | **[2026-08-19 신규]** ✅ 통과 — `16`의 `ProcessStoreType`을 재사용해 미선언 키 접근 2건(읽기, 메소드 체이닝)이 정확히 `TypeError`로 거부됨을 확인, 양성 경로(선언된 키 3개) 클린. `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측 확정 |
|
| `21-type-store-undeclared-key-rejected` | **[2026-08-19 신규]** ✅ 통과 — `16`의 `ProcessStoreType`을 재사용해 미선언 키 접근 2건(읽기, 메소드 체이닝)이 정확히 `TypeError`로 거부됨을 확인, 양성 경로(선언된 키 3개) 클린. `store-plan.md`가 "아마 그럴 것"으로만 적어뒀던 걸 M0에서 실측 확정 |
|
||||||
| `23-type-quadtypes-checkversion-addplugin` | **[2026-08-19 신규]** ✅ 통과 — 실제 `quad-types`/`quad-base`로 `CheckedQuad<T>`+`AddPlugin<Self,P>` 통합 검증. 재작성 과정에서 `type function`을 거친 값은 패스스루라도 이후 제네릭 self 체이닝이 조용히 깨진다는 새 Luau 함정 발견(`typing-limits.md` §6으로 승격) — 최종 설계(별도 가상 필드로 격리)는 양성/음성 경로 모두 클린 |
|
| `23-type-quadtypes-checkversion-addplugin` | **[2026-08-19 신규, 같은 날 후속으로 재작성]** ✅ 통과 — 실제 `quad-types`/`quad-base`/`type-version-check`로 `CheckedQuad<T, Pattern>`+`AddPlugin<Self,P>` 통합 검증. 재작성 과정에서 `type function`을 거친 값은 패스스루라도 이후 제네릭 self 체이닝이 조용히 깨진다는 새 Luau 함정 발견(`typing-limits.md` §6으로 승격), `export type function`/이중 꺾쇠 제네릭 인스턴스화 요구도 같이 실측 — 최종 설계(별도 가상 필드로 격리)는 양성/음성 경로 모두 클린 |
|
||||||
|
|
||||||
### 특별히 중요한 통과 3건
|
### 특별히 중요한 통과 3건
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
--!strict
|
--!strict
|
||||||
--[[
|
--[[
|
||||||
검증 대상(타입 전용): `quad-types`의 `CheckedQuad<T>`(버전 체크
|
검증 대상(타입 전용): `quad-types`의 `CheckedQuad<T, Pattern>`(버전
|
||||||
type function)가 실제 `quad-base`가 구현하는 `Quad` 타입과 맞물려
|
패턴 체크, `type-version-check` 패키지 위에 얹힘)가 실제 `quad-base`가
|
||||||
동작하는지 — 그리고 검증 후에도 `AddPlugin<Self,P>`의 제네릭 체이닝이
|
구현하는 `Quad` 타입과 맞물려 동작하는지 — 그리고 검증 후에도
|
||||||
안 깨지는지.
|
`AddPlugin<Self,P>`의 제네릭 체이닝이 안 깨지는지.
|
||||||
|
|
||||||
배경: 2026-08-19 세션 대화 — quad-roblox가 `QuadRoblox(Quad):
|
배경: 2026-08-19 세션 대화 — quad-roblox가 `QuadRoblox(Quad):
|
||||||
QuadRoblox`처럼 quad-base 인스턴스를 **런타임 주입**으로 받게 되면서,
|
QuadRoblox`처럼 quad-base 인스턴스를 **런타임 주입**으로 받게 되면서,
|
||||||
pesde의 semver 충돌 방지가 이 관계엔 안 걸리게 됨(선언된 의존성이
|
pesde의 semver 충돌 방지가 이 관계엔 안 걸리게 됨(선언된 의존성이
|
||||||
아니라 그냥 함수 인자라서) — 그 빈 자리를 메꾸는 컴파일 타임 버전
|
아니라 그냥 함수 인자라서) — 그 빈 자리를 메꾸는 컴파일 타임 버전
|
||||||
체크. `quad-types/src/init.luau`의 `CheckVersion`/`CheckedQuad` 절
|
체크. `quad-types/src/init.luau`의 `CheckedQuad` 절,
|
||||||
참고.
|
`type-version-check/src/init.luau`의 `CheckVersion` 절 참고.
|
||||||
|
|
||||||
**이 파일이 재현하는 핵심 함정(처음 시도했다가 깨진 것들)**:
|
**이 파일이 재현하는 핵심 함정(처음 시도했다가 깨진 것들)**:
|
||||||
1. `error()`는 못 씀 — type function 자체가 실패한 걸로 판정돼서
|
1. `error()`는 못 씀 — type function 자체가 실패한 걸로 판정돼서
|
||||||
|
|
@ -20,15 +20,20 @@
|
||||||
CheckVersion<T>`) 제네릭이 인스턴스화될 때마다 재평가되지 않아
|
CheckVersion<T>`) 제네릭이 인스턴스화될 때마다 재평가되지 않아
|
||||||
진단이 아예 안 뜬다 — 리턴 타입/필드 타입처럼 **호출부마다 실제로
|
진단이 아예 안 뜬다 — 리턴 타입/필드 타입처럼 **호출부마다 실제로
|
||||||
해석돼야 하는 자리**에 박아 넣어야 함.
|
해석돼야 하는 자리**에 박아 넣어야 함.
|
||||||
3. **[가장 중요, 가장 늦게 발견]** `CheckVersion<T>`가 `T`를 단순
|
3. **[가장 중요]** `CheckVersion<T>`가 `T`를 단순 패스스루(`return
|
||||||
패스스루(`return t`)해도, 그 결과 타입이 **한 번이라도 type
|
t`)해도, 그 결과 타입이 **한 번이라도 type function을 거쳤다는
|
||||||
function을 거쳤다는 이력만으로** 이후 `AddPlugin<Self,P>` 같은
|
이력만으로** 이후 `AddPlugin<Self,P>` 같은 제네릭 self 메소드
|
||||||
제네릭 self 메소드 체이닝이 조용히 깨짐("Expected this to be
|
체이닝이 조용히 깨짐. 검증 결과는 **원본 타입과 절대 안 섞이는
|
||||||
exactly 'P & Self', but got 'P & Self'"류의 앞뒤가 같은 의미 없는
|
별도 가상 필드**(`__versionCheck`)로 격리해야 하고, 그 필드를
|
||||||
진단). 그래서 검증 결과는 **원본 타입과 절대 안 섞이는 별도
|
실제로 참조해야만 평가가 일어난다(lazy) — 이 파일의 `_forceCheck`
|
||||||
가상 필드**(`__versionCheck`)로 격리해야 하고, 그 필드를 실제로
|
줄이 그 필수 스텝을 보여줌.
|
||||||
참조해야만 평가가 일어난다(lazy) — 이 파일의 `_forceCheck` 줄이
|
4. **[신규]** cross-package 사용에는 `type function` 선언에 `export`가
|
||||||
그 필수 스텝을 보여줌.
|
필요하다(`type function`이 아니라 `export type function`) — 안
|
||||||
|
그러면 다른 파일에서 `Unknown type 'Module.CheckVersion'`으로
|
||||||
|
막힘. 그리고 명시적 제네릭 인스턴스화가 **2개 이상**이면 단일
|
||||||
|
꺾쇠(`Foo<A, B>`)가 비교 연산자로 오파싱돼 반드시 이중 꺾쇠
|
||||||
|
(`Foo<<A, B>>`)를 써야 한다(코퍼스에 이미 있던 `AttributeKey<<T>>`
|
||||||
|
관례와 같은 이유).
|
||||||
|
|
||||||
실행: `luau-analyze 23-type-quadtypes-checkversion-addplugin.luau`
|
실행: `luau-analyze 23-type-quadtypes-checkversion-addplugin.luau`
|
||||||
]]
|
]]
|
||||||
|
|
@ -38,8 +43,9 @@ local QuadTypes = require("../../../quad-types/src")
|
||||||
type RobloxExt = { Frame: (self: any) -> string }
|
type RobloxExt = { Frame: (self: any) -> string }
|
||||||
|
|
||||||
-- quad-roblox가 실제로 쓰게 될 패턴 — 검증과 확장은 별도 스텝(합치면 3번
|
-- quad-roblox가 실제로 쓰게 될 패턴 — 검증과 확장은 별도 스텝(합치면 3번
|
||||||
-- 함정에 걸림)
|
-- 함정에 걸림). 여기선 quad-base와 정확히 같은 버전만 허용("0.0.0" 그대로) —
|
||||||
local function CheckQuad<T>(quad: T): QuadTypes.CheckedQuad<T>
|
-- quad-spring-roblox류 느슨한 소비자는 "0.*.*" 같은 패턴을 대신 씀.
|
||||||
|
local function CheckQuad<T>(quad: T): QuadTypes.CheckedQuad<T, "0.0.0">
|
||||||
return quad :: any
|
return quad :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
# 2026-08-19, 여덟 번째 세션 — `type-version-check` 패키지 추출, `CheckedQuad<T, Pattern>` 확장
|
||||||
|
|
||||||
|
**요약**: 직전(일곱 번째) 세션이 만든 `CheckedQuad<T>`(정확 버전 일치만
|
||||||
|
지원)가 `quad-spring`/`quad-spring-roblox`류 독립 게시 플러그인엔 너무
|
||||||
|
빡빡하다는 사용자 지적으로 시작. 글롭(`"*"`)/캐럿(`"N^"`) 패턴 매칭을
|
||||||
|
지원하도록 `CheckedQuad<T, Pattern>`으로 확장하고, 그 매칭 로직 자체는
|
||||||
|
quad에 종속되지 않은 범용 워크스페이스 패키지 `type-version-check`로
|
||||||
|
분리·구현·검증까지 완료. 사용자 지시대로 지금은 quad 모노레포 안에
|
||||||
|
두고, 독립 저장소 분리는 `HUMAN_TODO.md`에 남김.
|
||||||
|
|
||||||
|
## 1. 문제 제기 — 정확 일치는 독립 게시 플러그인엔 과함
|
||||||
|
|
||||||
|
사용자 발언 요지: "quad-spring/quad-spring-roblox 같은건 버전 확인이
|
||||||
|
exactly 할 필요는 없다고 봄" — 최신 `quad-spring-roblox`가 과거
|
||||||
|
`quad-spring` 버전도 잘 다룰 가능성이 높으므로, 글롭(`"3.*.*"`)이나
|
||||||
|
캐럿(`"3.3^.4^"`, 마이너 3 이상 + 패치 4 이상) 패턴을 지원하는 게
|
||||||
|
"구현하기 정말 쉽고... 있으면 좋다"는 판단. 같이 제안된 것: (1) 미래
|
||||||
|
`quad-roblox-types` 패키지(지금 만들 필요는 없음, 다만 `quad-roblox`의
|
||||||
|
공개 타입을 지금부터 단일 파일에 몰아둬서 나중에 쉽게 분리되게만
|
||||||
|
준비), (2) 버전 체크 패턴 자체를 `qwreey/type-version-check`로 독립
|
||||||
|
추출(다른 프로젝트에도 쓸 수 있고, quad-spring-roblox류가 이것 때문에
|
||||||
|
quad-base 전체를 끌고 올 필요가 없어짐), (3) Luau 내장 `index<>` type
|
||||||
|
function으로 `Version` 필드를 뽑는 게 수동 `readproperty`보다 나아
|
||||||
|
보인다는 제안.
|
||||||
|
|
||||||
|
**사용자 명시적 지시**: "우선 이 프로젝트 안에 넣어둬줘. 나중에 내가
|
||||||
|
다른 프로젝트로 분리해줄게. Human todo 로 남기면 될듯 함."
|
||||||
|
|
||||||
|
## 2. `type-version-check` 패키지 구현
|
||||||
|
|
||||||
|
새 워크스페이스 멤버(`[target] environment = "luau"` — quad에 종속되지
|
||||||
|
않아 다른 멤버와 달리 roblox가 아님). 핵심:
|
||||||
|
|
||||||
|
- 런타임 유틸 `matchesPattern(actual, pattern)` — `.`로 나눈 각 자리를
|
||||||
|
`"*"`(와일드카드) / `"N^"`(그 자리 숫자값이 N 이상이면 통과) / 그 외
|
||||||
|
정확 일치로 비교.
|
||||||
|
- `export type function CheckVersion(actual: type, pattern: type): type` —
|
||||||
|
`actual`/`pattern`을 문자열 리터럴로 검증 후 같은 매칭 로직을 타입
|
||||||
|
레벨에서 재현, 성공 시 `types.singleton(true)`만 반환(원본 타입
|
||||||
|
패스스루 금지 — 직전 세션이 확정한 함정 회피 원칙 그대로 유지).
|
||||||
|
|
||||||
|
**새로 발견한 Luau 함정 2건** (이전 세션들의 `typing-limits.md` §6과는
|
||||||
|
다른 결의 순수 문법 제약):
|
||||||
|
1. `type function`은 같은 파일의 바깥 스코프 로컬 함수를 못
|
||||||
|
참조한다(`Type function cannot reference outer local 'X'`) —
|
||||||
|
`matchesPattern`과 `CheckVersion` 내부의 매칭 로직은 물리적으로
|
||||||
|
중복된 별개 함수로 유지해야 함.
|
||||||
|
2. cross-package 사용엔 `type function`이 아니라 `export type
|
||||||
|
function`이 필요(안 그러면 `Unknown type 'Module.X'`), 그리고 명시적
|
||||||
|
제네릭 인스턴스화가 2개 이상이면 단일 꺾쇠가 비교 연산자로
|
||||||
|
오파싱되므로 이중 꺾쇠(`Foo<<A, B>>`)가 필요(코퍼스의 기존
|
||||||
|
`AttributeKey<<T>>` 관례와 동일 이유).
|
||||||
|
|
||||||
|
`index<T, "Version">` 내장 type function으로 `Version` 필드 추출 —
|
||||||
|
사용자 제안대로 채택, 단독 스파이크와 `CheckVersion`에 실제로 물려서
|
||||||
|
둘 다 실측 확인.
|
||||||
|
|
||||||
|
selene `shadowing` 경고(중첩 스코프의 `actualValue` 재선언, 두 곳)를
|
||||||
|
안쪽 변수를 `actualNumber`로 리네임해 해소.
|
||||||
|
|
||||||
|
## 3. `quad-types` 쪽 배선
|
||||||
|
|
||||||
|
`quad-types/pesde.toml`에 `type_version_check = { workspace =
|
||||||
|
"qwreey/type_version_check", version = "^", target = "luau" }` 추가 —
|
||||||
|
`target` 없이는 `pesde install`이 "no workspace member found with name
|
||||||
|
qwreey/type_version_check and target roblox"로 실패(quad-types 자신의
|
||||||
|
기본 target이 roblox라 명시적 target 지정이 필요, `quad-types`↔`type-
|
||||||
|
version-check`가 서로 다른 target을 가진 첫 워크스페이스 의존 관계라서
|
||||||
|
이번에 처음 실측됨).
|
||||||
|
|
||||||
|
`CheckedQuad<T> = T & { __versionCheck: CheckVersion<T> }` →
|
||||||
|
`CheckedQuad<T, Pattern> = T & { __versionCheck:
|
||||||
|
TypeVersionCheck.CheckVersion<index<T, "Version">, Pattern> }`로 확장.
|
||||||
|
|
||||||
|
심볼릭 링크 로컬 CLI 우회(`project-setup-plan.md`가 이미 문서화한
|
||||||
|
워크어라운드)를 2단 깊이 의존 그래프(`quad-base`/`quad-roblox` →
|
||||||
|
`quad-types` → `type-version-check`)에 재적용 — 문제없이 일반화됨을
|
||||||
|
확인.
|
||||||
|
|
||||||
|
## 4. 검증
|
||||||
|
|
||||||
|
전 패키지(`quad-base`/`quad-roblox`/`quad-types`/`type-version-check`)
|
||||||
|
`luau-analyze`/`luau`/`selene` 전부 클린. 스파이크
|
||||||
|
`23-type-quadtypes-checkversion-addplugin.luau`를 새 시그니처로
|
||||||
|
재작성해 재검증 — 양성 경로(버전 일치 + `AddPlugin` 2회 체이닝) 클린,
|
||||||
|
음성 경로(버전 불일치)는 정확히 `TypeError: type-version-check: version
|
||||||
|
"9.9.9" does not match pattern "0.0.0"` 하나만 발생.
|
||||||
|
|
||||||
|
## 5. 문서 반영
|
||||||
|
|
||||||
|
`base/quad-types-plan.md`(`type-version-check` 절 신설 + `CheckedQuad<T,
|
||||||
|
Pattern>` 섹션 갱신 + 남은 것에 `quad-roblox-types` 백로그/HUMAN_TODO
|
||||||
|
포인터 추가), `base/architecture.md`(소스 트리에 `type-version-check/`
|
||||||
|
추가, 패키징 방식 문단 갱신), `base/project-setup-plan.md`(cross-target
|
||||||
|
워크스페이스 의존 함정 + 2단 심볼릭 링크 우회 재확인), `base/typing-
|
||||||
|
limits.md`(§6 예시 코드를 `CheckedQuad<T, Pattern>`으로 갱신 + 절 인용
|
||||||
|
수정), `.claude/README.md`/`luau-test/README.md`/`luau-test/STATUS.md`
|
||||||
|
(스파이크 23 설명 갱신), 루트 `HUMAN_TODO.md`(9번 — `type-version-check`
|
||||||
|
독립 저장소 분리는 사용자 몫).
|
||||||
|
|
||||||
|
## 감사 루프 (2라운드, 핸드오버 체크리스트대로)
|
||||||
|
|
||||||
|
1라운드는 `quad-types-plan.md`/`type-version-check` 자신의 파일/주석에
|
||||||
|
남아있던 구 `CheckedQuad<T>`(콤마 없는 단일 파라미터) 잔존 4건과
|
||||||
|
`architecture.md`의 `pesde.toml` 나열 누락, `luau-test/STATUS.md` 배너
|
||||||
|
stale 1건을 찾아 전부 수정. 2라운드(각도를 인덱스 레이어/교차 참조로
|
||||||
|
전환)는 `project-setup-plan.md`의 옛 2-멤버 트리 다이어그램과 "3개
|
||||||
|
패키지"/"총 3개" 개수 하드코딩(멤버가 2→4로 늘어난 걸 못 따라간 자리
|
||||||
|
2곳)을 찾아 `architecture.md`를 소스로 가리키게 일반화. 이후 3라운드는
|
||||||
|
새 발견 0건 — 여기서 감사 루프 종료.
|
||||||
|
|
||||||
|
## 다음에 확인할 것
|
||||||
|
|
||||||
|
없음 — 이 턴의 설계/구현/검증/문서화/2라운드 감사까지 전부 마무리.
|
||||||
|
`quad-roblox-types`는 사용자가 명시적으로 후순위 지정(지금 안 만듦),
|
||||||
|
`type-version-check` 독립 분리는 사용자 본인이 나중에 직접 진행.
|
||||||
|
|
@ -161,6 +161,17 @@ Debounce/Throttle 작업에 쓴 워크트리는 **사용자 확인 후 정리
|
||||||
없는지, (3) `git worktree list`에 다른 에이전트 워크트리가 없는지를
|
없는지, (3) `git worktree list`에 다른 에이전트 워크트리가 없는지를
|
||||||
확인했습니다. 이 항목은 기록용으로만 남겨둡니다.
|
확인했습니다. 이 항목은 기록용으로만 남겨둡니다.
|
||||||
|
|
||||||
|
## 9. **[2026-08-19 신설, 안 막음]** `type-version-check` 독립 저장소로 분리
|
||||||
|
|
||||||
|
`type-version-check/`(컴파일 타임 버전 패턴 매칭 — 글롭/캐럿, `quad-types`의
|
||||||
|
`CheckedQuad<T, Pattern>`이 이 위에 얹힘)는 quad에 종속되지 않은 범용
|
||||||
|
유틸이라 **사용자가 직접 독립 저장소로 분리할 예정**("우선 이 프로젝트
|
||||||
|
안에 넣어둬줘. 나중에 내가 다른 프로젝트로 분리해줄게.", 2026-08-19).
|
||||||
|
지금은 quad 워크스페이스의 네 번째 멤버(`workspace_members`)로만 있음 —
|
||||||
|
에이전트가 먼저 나서서 분리하지 말고 사용자가 하라고 할 때까지 대기.
|
||||||
|
설계/구현 상세는 `.claude/base/quad-types-plan.md`의
|
||||||
|
"`type-version-check`" 절.
|
||||||
|
|
||||||
## 3. `.claude/question.md`의 **나머지** 항목 검토 (급하지 않음)
|
## 3. `.claude/question.md`의 **나머지** 항목 검토 (급하지 않음)
|
||||||
|
|
||||||
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
|
디자인 결정 중 Lua/Roblox 엔진에 대한 깊은 경험이 필요한 것들은 합리적 기본값으로
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,6 @@ roblox = "quad-roblox"
|
||||||
|
|
||||||
[workspace."qwreey/quad_types"]
|
[workspace."qwreey/quad_types"]
|
||||||
roblox = "quad-types"
|
roblox = "quad-types"
|
||||||
|
|
||||||
|
[workspace."qwreey/type_version_check"]
|
||||||
|
luau = "type-version-check"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
name = "qwreey/quad"
|
name = "qwreey/quad"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
private = true
|
private = true
|
||||||
workspace_members = ["quad-base", "quad-roblox", "quad-types"]
|
workspace_members = ["quad-base", "quad-roblox", "quad-types", "type-version-check"]
|
||||||
|
|
||||||
[target]
|
[target]
|
||||||
environment = "roblox"
|
environment = "roblox"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ target = "roblox"
|
||||||
[graph."qwreey/quad_types@0.0.0 roblox"]
|
[graph."qwreey/quad_types@0.0.0 roblox"]
|
||||||
direct = ["quad_types", { workspace = "qwreey/quad_types", version = "^" }, "standard"]
|
direct = ["quad_types", { workspace = "qwreey/quad_types", version = "^" }, "standard"]
|
||||||
|
|
||||||
|
[graph."qwreey/quad_types@0.0.0 roblox".dependencies]
|
||||||
|
type_version_check = ["qwreey/type_version_check@0.0.0 luau", "standard"]
|
||||||
|
|
||||||
[graph."qwreey/quad_types@0.0.0 roblox".pkg_ref]
|
[graph."qwreey/quad_types@0.0.0 roblox".pkg_ref]
|
||||||
ref_ty = "workspace"
|
ref_ty = "workspace"
|
||||||
path = "quad-types"
|
path = "quad-types"
|
||||||
|
|
||||||
|
[graph."qwreey/quad_types@0.0.0 roblox".pkg_ref.dependencies]
|
||||||
|
type_version_check = [{ workspace = "qwreey/type_version_check", version = "^", target = "luau" }, "standard"]
|
||||||
|
|
||||||
|
[graph."qwreey/type_version_check@0.0.0 luau".pkg_ref]
|
||||||
|
ref_ty = "workspace"
|
||||||
|
path = "type-version-check"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,16 @@ target = "roblox"
|
||||||
[graph."qwreey/quad_types@0.0.0 roblox"]
|
[graph."qwreey/quad_types@0.0.0 roblox"]
|
||||||
direct = ["quad_types", { workspace = "qwreey/quad_types", version = "^" }, "standard"]
|
direct = ["quad_types", { workspace = "qwreey/quad_types", version = "^" }, "standard"]
|
||||||
|
|
||||||
|
[graph."qwreey/quad_types@0.0.0 roblox".dependencies]
|
||||||
|
type_version_check = ["qwreey/type_version_check@0.0.0 luau", "standard"]
|
||||||
|
|
||||||
[graph."qwreey/quad_types@0.0.0 roblox".pkg_ref]
|
[graph."qwreey/quad_types@0.0.0 roblox".pkg_ref]
|
||||||
ref_ty = "workspace"
|
ref_ty = "workspace"
|
||||||
path = "quad-types"
|
path = "quad-types"
|
||||||
|
|
||||||
|
[graph."qwreey/quad_types@0.0.0 roblox".pkg_ref.dependencies]
|
||||||
|
type_version_check = [{ workspace = "qwreey/type_version_check", version = "^", target = "luau" }, "standard"]
|
||||||
|
|
||||||
|
[graph."qwreey/type_version_check@0.0.0 luau".pkg_ref]
|
||||||
|
ref_ty = "workspace"
|
||||||
|
path = "type-version-check"
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,10 @@ format = 2
|
||||||
name = "qwreey/quad_types"
|
name = "qwreey/quad_types"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
target = "roblox"
|
target = "roblox"
|
||||||
|
|
||||||
|
[graph."qwreey/type_version_check@0.0.0 luau"]
|
||||||
|
direct = ["type_version_check", { workspace = "qwreey/type_version_check", version = "^", target = "luau" }, "standard"]
|
||||||
|
|
||||||
|
[graph."qwreey/type_version_check@0.0.0 luau".pkg_ref]
|
||||||
|
ref_ty = "workspace"
|
||||||
|
path = "type-version-check"
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,6 @@ includes = ["src/*"]
|
||||||
environment = "roblox"
|
environment = "roblox"
|
||||||
build_files = ["src"]
|
build_files = ["src"]
|
||||||
lib = "src/init.luau"
|
lib = "src/init.luau"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
type_version_check = { workspace = "qwreey/type_version_check", version = "^", target = "luau" }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
(`.claude/base/project-setup-plan.md`/`session/2026-08-19-07-*.md` 참고)
|
(`.claude/base/project-setup-plan.md`/`session/2026-08-19-07-*.md` 참고)
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
local TypeVersionCheck = require("./luau_packages/type_version_check")
|
||||||
|
|
||||||
export type Quad = {
|
export type Quad = {
|
||||||
Version: "0.0.0", -- quad-base/pesde.toml의 version과 항상 맞출 것
|
Version: "0.0.0", -- quad-base/pesde.toml의 version과 항상 맞출 것
|
||||||
debug: boolean,
|
debug: boolean,
|
||||||
|
|
@ -24,57 +26,30 @@ export type Quad = {
|
||||||
}
|
}
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
`CheckVersion<T>` — 주입된 값의 실제 타입 `T`가 이 패키지가 아는
|
`CheckedQuad<T, Pattern>` — 주입된 값의 실제 타입 `T`가 `Pattern`
|
||||||
`Quad.Version`과 일치하는지 컴파일 타임에 확인.
|
(글롭/캐럿 문자열, `type-version-check` 패키지 참고 — `"*"`/`"N^"`/정확값)에
|
||||||
|
맞는 `Version`을 갖는지 컴파일 타임에 확인. quad-base/quad-roblox
|
||||||
|
자신은 항상 같은 모노레포에서 같이 개발되므로 지금은 정확히 일치하는
|
||||||
|
`"0.0.0"` 패턴으로만 쓰지만(아래 사용법), quad-spring-roblox류
|
||||||
|
**독립적으로 게시되는 백엔드 플러그인**은 `"0.*.*"`처럼 느슨한 패턴을
|
||||||
|
직접 골라 쓸 수 있다 — 그래서 패턴을 이 타입의 파라미터로 열어둠.
|
||||||
|
|
||||||
**⚠️ 반드시 "가상 필드"로만 쓸 것 — `T`를 직접 패스스루하지 말 것.**
|
**⚠️ 반드시 "가상 필드"로만 쓸 것 — `T`를 직접 패스스루하지 말 것.**
|
||||||
처음엔 `return t`(단순 패스스루)로 짜서 단독으로는 잘 됐는데,
|
`TypeVersionCheck.CheckVersion<Actual, Pattern>` 자체가 이미 이 규칙을
|
||||||
**`AddPlugin<Self,P>`처럼 제네릭 self 파라미터를 쓰는 메소드와
|
지키게 설계돼 있다(트리비얼한 `true`만 반환, `T`를 절대 참조/재구성
|
||||||
체이닝하면 조용히 깨짐**을 실측으로 확인함(2026-08-19) — 값의 정적
|
안 함) — `type function`을 거친 값은 패스스루라도 이후 `AddPlugin`
|
||||||
타입이 한 번이라도 `type function`을 거치면, 그 뒤 제네릭 self 추론이
|
같은 제네릭 self 메소드 체이닝이 조용히 깨진다는 게 실측 확인됐기
|
||||||
"Expected this to be exactly 'P & Self', but got 'P & Self'"류의
|
때문(`typing-limits.md` §6, `type-version-check/src/init.luau`
|
||||||
앞뒤가 같은 의미 없는 진단을 내며 깨진다(재구성이 아니라 **패스스루도
|
참고).
|
||||||
똑같이** 깨짐 — 재구성 비용 문제가 아니라 "그 타입이 type function을
|
|
||||||
거쳤다는 이력 자체"가 문제였음). 그래서 검증 결과를 **원본 타입과
|
|
||||||
절대 안 섞이는 별도 필드**로 격리해야 한다 — `CheckVersion<T>` 자체는
|
|
||||||
`T`를 절대 참조/반환하지 않고, 성공 시 트리비얼한 `true` 하나만 반환.
|
|
||||||
|
|
||||||
**사용법(필수 — 이 필드를 실제로 참조해야 체크가 평가된다)**:
|
**사용법(필수 — 이 필드를 실제로 참조해야 체크가 평가된다)**:
|
||||||
```lua
|
```lua
|
||||||
type Checked<T> = T & { __versionCheck: CheckVersion<T> }
|
local checked: QuadTypes.CheckedQuad<typeof(injectedQuad), "0.0.0"> = injectedQuad :: any
|
||||||
|
|
||||||
local function CheckQuad<T>(quad: T): Checked<T>
|
|
||||||
return quad :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
local checked = CheckQuad(injectedQuad)
|
|
||||||
local _ = checked.__versionCheck -- ⚠️ 이 줄이 없으면 체크가 조용히 스킵됨(lazy 평가)
|
local _ = checked.__versionCheck -- ⚠️ 이 줄이 없으면 체크가 조용히 스킵됨(lazy 평가)
|
||||||
-- 이후 checked는 injectedQuad와 완전히 같은 타입(원본 그대로) —
|
-- 이후 checked는 injectedQuad와 완전히 같은 타입(원본 그대로) —
|
||||||
-- AddPlugin 체이닝 등 뒤이은 제네릭 연산이 전부 안전하게 동작함
|
-- AddPlugin 체이닝 등 뒤이은 제네릭 연산이 전부 안전하게 동작함
|
||||||
```
|
```
|
||||||
|
|
||||||
**에러 표시 방식(실측 확인)**: 불일치 시 `error()`가 아니라
|
|
||||||
`print("메시지")` + `return types.never`를 쓸 것 — `error()`는 "type
|
|
||||||
function 자체가 실패함"으로 판정돼 버려서 못 쓰고, `print`+`never`
|
|
||||||
조합만 호출부에 정확히 "TypeError: <메시지>"로 뜬다.
|
|
||||||
]]
|
]]
|
||||||
-- selene: allow(undefined_variable) — `types`는 `type function` 블록 안에서만
|
export type CheckedQuad<T, Pattern> = T & { __versionCheck: TypeVersionCheck.CheckVersion<index<T, "Version">, Pattern> }
|
||||||
-- 주입되는 특수 전역이라 selene의 luau std가 아직 이걸 모델링 못 함(실측
|
|
||||||
-- 확인, 2026-08-19) — 실제 미정의 변수가 아니라 selene 쪽 오탐.
|
|
||||||
type function CheckVersion(t: type): type
|
|
||||||
local versionProp = t:readproperty(types.singleton("Version"))
|
|
||||||
if versionProp == nil then
|
|
||||||
print("quad-base injection is missing a Version field")
|
|
||||||
return types.never
|
|
||||||
end
|
|
||||||
local literal = versionProp:value()
|
|
||||||
if literal ~= "0.0.0" then
|
|
||||||
print(`quad-base version mismatch: this package expects "0.0.0", got "{tostring(literal)}"`)
|
|
||||||
return types.never
|
|
||||||
end
|
|
||||||
return types.singleton(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
export type CheckedQuad<T> = T & { __versionCheck: CheckVersion<T> }
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
6
type-version-check/pesde.lock
Normal file
6
type-version-check/pesde.lock
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# This file is automatically @generated by pesde.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
format = 2
|
||||||
|
name = "qwreey/type_version_check"
|
||||||
|
version = "0.0.0"
|
||||||
|
target = "luau"
|
||||||
8
type-version-check/pesde.toml
Normal file
8
type-version-check/pesde.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
name = "qwreey/type_version_check"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = "컴파일 타임 버전 패턴 매칭 — type function 하나로 semver 비슷한 문자열 리터럴 타입을 글롭(*)/캐럿(N^) 패턴과 대조. quad에 종속되지 않은 범용 유틸(quad-types의 CheckedQuad<T, Pattern>이 이 위에 얹힘). 현재는 quad 모노레포 안 워크스페이스 멤버로 두지만, 다른 프로젝트에서도 쓸 수 있게 독립 패키지로 분리 예정(HUMAN_TODO.md 참고) — 지금부터 quad 전용 타입/이름을 안 섞을 것."
|
||||||
|
includes = ["src/*"]
|
||||||
|
|
||||||
|
[target]
|
||||||
|
environment = "luau"
|
||||||
|
lib = "src/init.luau"
|
||||||
31
type-version-check/selene.toml
Normal file
31
type-version-check/selene.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
std = "luau"
|
||||||
|
exclude = ["**/.pesde/*", "*_packages/*"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
almost_swapped = "deny"
|
||||||
|
constant_table_comparison = "deny"
|
||||||
|
deprecated = "warn"
|
||||||
|
divide_by_zero = "warn"
|
||||||
|
empty_if = "deny"
|
||||||
|
empty_loop = "deny"
|
||||||
|
high_cyclomatic_complexity = "warn"
|
||||||
|
if_same_then_else = "deny"
|
||||||
|
ifs_same_cond = "warn"
|
||||||
|
incorrect_standard_library_use = "deny"
|
||||||
|
manual_table_clone = "warn"
|
||||||
|
mismatched_arg_count = "deny"
|
||||||
|
mixed_table = "allow"
|
||||||
|
multiple_statements = "deny"
|
||||||
|
must_use = "warn"
|
||||||
|
parenthese_conditions = "deny"
|
||||||
|
suspicious_reverse_loop = "deny"
|
||||||
|
type_check_inside_call = "deny"
|
||||||
|
unbalanced_assignments = "warn"
|
||||||
|
undefined_variable = "deny"
|
||||||
|
unscoped_variables = "deny"
|
||||||
|
unused_variable = "warn"
|
||||||
|
|
||||||
|
[config]
|
||||||
|
empty_if = { comments_count = true }
|
||||||
|
empty_loop = { comments_count = true }
|
||||||
|
multiple_statements = { one_line_if = "break-return-only" }
|
||||||
130
type-version-check/src/init.luau
Normal file
130
type-version-check/src/init.luau
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
--!strict
|
||||||
|
--[[
|
||||||
|
컴파일 타임 버전 패턴 매칭 — `quad`에 종속되지 않은 범용 유틸.
|
||||||
|
|
||||||
|
**[2026-08-19 신설, quad 저장소 안에 임시로 둠]** 지금은 quad
|
||||||
|
워크스페이스의 네 번째 멤버로 두지만, 사용자가 나중에 독립 저장소로
|
||||||
|
직접 분리할 예정(`HUMAN_TODO.md` 참고) — 그래서 이 파일 안에 `Quad`류
|
||||||
|
quad 전용 이름/타입을 절대 안 섞는다. `quad-types`의
|
||||||
|
`CheckedQuad<T, Pattern>`이 이 위에 얹히는 소비자 중 하나일 뿐.
|
||||||
|
|
||||||
|
**패턴 문법**: `.`로 나뉜 각 자리가
|
||||||
|
- `"*"` — 와일드카드(뭐든 통과)
|
||||||
|
- `"N^"` — 그 자리 값이 숫자로 봤을 때 N **이상**이면 통과(caret)
|
||||||
|
- 그 외 — 정확히 같은 문자열이어야 통과
|
||||||
|
|
||||||
|
예: `"3.*.*"`(메이저만 고정), `"3.3^.4^"`(마이너 3 이상 + 패치
|
||||||
|
4 이상), `"0.0.0"`(정확히 일치, quad-types의 `CheckedQuad`가 지금
|
||||||
|
쓰는 방식).
|
||||||
|
|
||||||
|
**⚠️ `matchesPattern`은 아래 `type function` 안에 통째로 다시
|
||||||
|
적혀 있다(중복) — 실측 확인된 제약**: `type function`은 같은 파일의
|
||||||
|
바깥 스코프 로컬 함수를 아예 참조 못 한다(`Type function cannot
|
||||||
|
reference outer local 'X'`로 컴파일 자체가 실패함). 그래서 순수
|
||||||
|
런타임 유틸(아래, 테스트/문서화/일반 사용 목적)과 `type function`
|
||||||
|
내부 구현은 로직이 같아도 물리적으로 별개 함수여야 한다 — 하나를
|
||||||
|
고치면 반드시 다른 하나도 같이 고칠 것.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local function matchesPattern(actual: string, pattern: string): boolean
|
||||||
|
local actualParts = string.split(actual, ".")
|
||||||
|
local patternParts = string.split(pattern, ".")
|
||||||
|
if #actualParts ~= #patternParts then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, patternPart in patternParts do
|
||||||
|
local actualPart = actualParts[i]
|
||||||
|
if patternPart == "*" then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if string.sub(patternPart, -1) == "^" then
|
||||||
|
local minValue = tonumber(string.sub(patternPart, 1, -2))
|
||||||
|
local actualNumber = tonumber(actualPart)
|
||||||
|
if minValue == nil or actualNumber == nil or actualNumber < minValue then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if actualPart ~= patternPart then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
`CheckVersion<Actual, Pattern>` — `Actual`/`Pattern` 둘 다 문자열
|
||||||
|
리터럴(singleton) 타입이어야 함. 일치하면 트리비얼한 `true` 하나만
|
||||||
|
반환하고, 불일치하면 `print`+`types.never`로 사람이 읽을 메시지를
|
||||||
|
낸다(`error()`는 쓰지 않음 — `typing-limits.md` §6/`quad-types-plan.md`
|
||||||
|
참고, type function 안에서 `error()`를 쓰면 "type function 자체가
|
||||||
|
실패함"으로 판정돼 버려짐).
|
||||||
|
|
||||||
|
**⚠️ 이 type function이 반환하는 값은 절대 원본 타입을 담지 않는다** —
|
||||||
|
`Actual`/`Pattern`을 그대로도, 재구성해서도 반환하지 않고 트리비얼한
|
||||||
|
`true`만 반환한다. 소비자는 이 결과를 원본과 절대 안 섞이는 별도
|
||||||
|
필드로만 노출할 것 — `type function`을 거친 값에 제네릭 self
|
||||||
|
메소드(`AddPlugin<Self,P>`류)를 나중에 부르면 조용히 깨지는 게 실측
|
||||||
|
확인됐기 때문(`typing-limits.md` §6). 예시는 `quad-types`의
|
||||||
|
`CheckedQuad<T, Pattern>` 구현 참고.
|
||||||
|
]]
|
||||||
|
-- selene: allow(undefined_variable) — `types`는 `type function` 블록 안에서만
|
||||||
|
-- 주입되는 특수 전역(quad-types/src/init.luau와 같은 이유의 오탐).
|
||||||
|
export type function CheckVersion(actual: type, pattern: type): type
|
||||||
|
local actualOk, actualValue = pcall(function()
|
||||||
|
return actual:value()
|
||||||
|
end)
|
||||||
|
if not actualOk or type(actualValue) ~= "string" then
|
||||||
|
print("type-version-check: expected a string literal type for the actual version")
|
||||||
|
return types.never
|
||||||
|
end
|
||||||
|
|
||||||
|
local patternOk, patternValue = pcall(function()
|
||||||
|
return pattern:value()
|
||||||
|
end)
|
||||||
|
if not patternOk or type(patternValue) ~= "string" then
|
||||||
|
print("type-version-check: expected a string literal type for the version pattern")
|
||||||
|
return types.never
|
||||||
|
end
|
||||||
|
|
||||||
|
-- matchesPattern과 로직 동일 — 위 경고문 참고, 바깥 함수를 못 불러서 복제함
|
||||||
|
local function matches(actualStr: string, patternStr: string): boolean
|
||||||
|
local actualParts = string.split(actualStr, ".")
|
||||||
|
local patternParts = string.split(patternStr, ".")
|
||||||
|
if #actualParts ~= #patternParts then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for i, patternPart in patternParts do
|
||||||
|
local actualPart = actualParts[i]
|
||||||
|
if patternPart == "*" then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
if string.sub(patternPart, -1) == "^" then
|
||||||
|
local minValue = tonumber(string.sub(patternPart, 1, -2))
|
||||||
|
local actualNumber = tonumber(actualPart)
|
||||||
|
if minValue == nil or actualNumber == nil or actualNumber < minValue then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if actualPart ~= patternPart then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if not matches(actualValue :: string, patternValue :: string) then
|
||||||
|
print(`type-version-check: version "{actualValue}" does not match pattern "{patternValue}"`)
|
||||||
|
return types.never
|
||||||
|
end
|
||||||
|
|
||||||
|
return types.singleton(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
matchesPattern = matchesPattern,
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue