quad/scripts/relink.sh
qwreey eb321e1747
qa: 7라운드 발견 52건 처리·반영 — Store 재설계 시도와 철회, 타입 함수 원칙 신설
`qa-request/pre-implementation-handtrace-round7.md`의 `H-55`~`H-106`을
사용자와 대화형으로 처리하고 `base/`에 전량 반영했다. 결정의 소스는
`-followup.md`(개수·개별 항목은 여기서 세지 않는다).

## 처분

확정 39 / 무효·소멸 4(`H-73`~`H-76`) / 기각 1(`H-77`) / 범위 축소 2 /
다른 항목으로 흡수 6.

**부수로 `question.md` 최우선 절이 비었다** — 중간 State GC는 `_hold`
불변식(하류 → 상류 강함)으로, 동적 키 표면 위치는 `store:Of<<T>>(name)`
하나로 닫혔다. **M2 착수를 막는 항목이 없다.**

## 구조가 바뀐 것

- `Ref`가 `Epoch`를 만족(`.Revision` + `EpochBrand`) — 포탈 캐치업 비대칭과
  같은 `Ref` 중복 dep이 같이 닫힘
- `Weak*` 등록 표면 신설(`Ref:WeakCallback` / `Observer:WeakSubscribe`) —
  Weak 쪽이 프리미티브고 강한 쪽이 "GC 킵"을 얹은 것
- `Effect`: dep 등록이 생성자 한 곳으로, 강한 주인은 `_deps` 하나,
  억제는 사적 `Blocker`, `bindLifetime`은 핸들 하나에만 적용.
  `:Rerun()` 정의 신설(재진입은 지연 재실행), `_installed` 신설
- 전파 루프 의사코드 확정 — 구독자 집합의 원소는 Observer **값**이고
  **자식 State 노드는 `canExecute`를 안 탄다**(그대로 짜면 파생 State
  아래가 전부 침묵했다)
- `rawInvalid` → `cacheTargetCount`/`cacheCurrCount` 카운터 쌍
- `recompute` 재진입 차단 + `invalidAfter` 되감기, `gatedRecompute`가
  인덱스를 캡처하지 않고 토큰으로 조회
- `emit(commit) -> boolean`, `EpochMap:Peek` — 정책이 흡수 집합을
  버리지도 읽지도 못하던 것을 닫음
- error 계약(`level` 이분, 메시지는 영어)과 예외 계약(`pcall`로 안 감쌈)을
  `architecture.md`에 신설

## Store — 같은 날 재설계하고 철회했다

`H-75`/`H-76`으로 `WrapStore`/`ProcessStoreType`이 폐기되자 그 자리를
"`store.key`는 값, `store:Of(k)`가 프리미티브"로 채웠다가 **같은 날
철회**했다(`archive/store-value-field-redesign-withdrawn.md`). 살아남은 건
**명시적 초기화** 하나다. 최종형은 타입 인자에 `Source<T>`를 직접 쓰고
`store.key`는 평범한 레코드 필드이며 **타입 함수를 안 쓴다**.

철회 이유 중 하나가 원칙으로 승격됐다 — **"타입 함수는 타입이 못 잡는
문제를 에러로 띄우는 정도 이상으로 가지 않는다"**(`typing-limits.md` §0).
`index<>`/`keyof<>`도 Luau가 predefine한 타입 함수라 같은 함정을 갖는다.

## 툴체인

**`luau` CLI가 심볼릭 링크를 못 탄다**(디렉토리·파일 둘 다)는 것이 최소
재현으로 밝혀졌다 — pesde 워크스페이스 링크가 전부 심볼릭이라 스모크 2개가
안 돌았고 `luau-analyze`는 **조용히 통과**했다("거짓 클린"). `scripts/relink.sh`
+ `scripts/test.sh` 신설, 이제 스모크 셋 전부 PASS.

## 검증

`/code-review high` 2회(12건 + 14건)와 감사 8패스(12/6/6/4/13/3/6/0건)를
각도를 바꿔가며 돌렸고 전부 반영했다 — 마지막 패스가 무발견으로 수렴.
각 패스의 각도와 대표 발견은 `-followup.md`의 검증 절이 소스.
`doc-check.py` ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>

Claude-Session: https://claude.ai/code/session_012oLwATeQdq9TCFdENPutFG
2026-08-25 23:40:19 +09:00

91 lines
4.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# pesde 워크스페이스 링크를 **실제 복사**로 교체한다. 여러 번 돌려도 최신을 따라간다.
#
# 왜 필요한가: **`luau` CLI가 심볼릭 링크를 못 탄다.** 디렉토리 링크도 파일
# 링크도 안 되고, `luau-analyze`는 더 나쁘게 **조용히 통과**한다(모듈을 `any`로
# 떨어뜨리고 진단 0건 — "거짓 클린"). pesde의 워크스페이스 링크는 전부 디렉토리
# 심볼릭이라, `quad-base/src/init.luau`의 `require("./roblox_packages/quad_types")`가
# 그 링크에 닿는 순간 죽는다. 근거와 최소 재현은
# `.claude/qa-request/pre-implementation-handtrace-round7-followup.md`의 🅛 절.
#
# ⚠️ 이 스크립트가 지켜야 하는 것 셋(전부 `/code-review high`가 실제로 잡은 것):
# 1. **`cp -rL`** — 평범한 `cp -r`는 심볼릭을 **심볼릭 그대로** 복사한다.
# 중첩 패키지(quad_types 안의 type_version_check 링크)가 그대로 살아남아
# 같은 "거짓 클린"이 재발한다.
# 2. **스킵은 치명적으로** — 원본이 없어 건너뛴 채 성공으로 끝나면 테스트가
# 얼어붙은 스냅샷을 돈다. 이 스크립트가 막으려던 실패 모드 그 자체다.
# 3. **깊이 고정 글롭** — `find -path '*/.pesde/...'`의 `*`는 `/`를 넘어가
# 중첩 `.pesde`와 `src`까지 잡는다. 셸 글롭은 `/`를 안 넘으므로 그걸 쓴다.
set -euo pipefail
shopt -s nullglob
cd "$(dirname "$0")/.."
MANIFEST=".relink-manifest"
fail=0
note_fail() { echo "relink: $*" >&2; fail=1; }
# 0) 매니페스트가 없는데 이미 복사돼 있으면(도입 전에 한 번 돌린 트리 등)
# pesde 레이아웃에서 원본 매핑을 되살린다.
# 구조: <member>/{luau,roblox}_packages/.pesde/<owner>+<pkg>/<ver>/<pkg>/<entry>
if [ ! -f "$MANIFEST" ]; then
for d in */luau_packages/.pesde/*/*/* */roblox_packages/.pesde/*/*/*; do
[ -d "$d" ] || continue
pkg="$(basename "$d")"
member="${pkg//_/-}"
[ -d "$member" ] || continue
for e in "$d"/*; do
n="$(basename "$e")"
[ -e "$member/$n" ] || continue
printf '%s\t%s\n' "$e" "$(readlink -f "$member/$n")" >> "$MANIFEST"
done
done
fi
replaced=0
refreshed=0
# 1) 아직 심볼릭인 것 — 원본을 기록하고 복사로 교체. 중첩 링크가 남지 않도록
# `cp -rL`로 역참조하고, 남은 게 없을 때까지 반복한다.
while :; do
found=0
while IFS= read -r link; do
found=1
target="$(readlink -f "$link")"
if [ ! -e "$target" ]; then note_fail "dangling symlink: $link"; continue; fi
printf '%s\t%s\n' "$link" "$target" >> "$MANIFEST"
rm "$link"
cp -rL "$target" "$link"
replaced=$((replaced + 1))
done < <(find . -path ./.git -prune -o -type l -print | grep '/\.pesde/' || true)
[ "$found" -eq 0 ] && break
done
# 2) 이미 복사된 것 — 매니페스트를 보고 원본에서 다시 복사(워크스페이스 멤버를
# 고쳤을 때 테스트가 옛 스냅샷을 돌지 않게).
if [ -f "$MANIFEST" ]; then
sort -u "$MANIFEST" -o "$MANIFEST"
while IFS=$'\t' read -r link target; do
[ -n "$link" ] || continue
[ -L "$link" ] && continue # 방금 1)에서 처리됨
if [ ! -e "$target" ]; then note_fail "missing source: $target (for $link)"; continue; fi
parent="$(dirname "$link")"
if [ ! -d "$parent" ]; then note_fail "missing link parent: $parent — 매니페스트가 낡았다, $MANIFEST 를 지우고 pesde install 후 다시 돌릴 것"; continue; fi
rm -rf "$link"
cp -rL "$target" "$link"
refreshed=$((refreshed + 1))
done < "$MANIFEST"
fi
echo "relink: $replaced symlink(s) replaced, $refreshed copy/copies refreshed"
if [ "$fail" -ne 0 ]; then
echo "relink: 위 스킵 때문에 트리가 최신이 아니다 — 테스트를 신뢰하지 말 것" >&2
exit 1
fi
# 3) 최종 확인 — .pesde 아래에 심볼릭이 하나도 남으면 안 된다.
leftover="$(find . -path ./.git -prune -o -type l -print | grep '/\.pesde/' || true)"
if [ -n "$leftover" ]; then
echo "relink: 심볼릭이 남았다 (luau가 못 탄다):" >&2
echo "$leftover" >&2
exit 1
fi