tooling: 절 인용 규약 신설 + doc-check 절 참조를 ERROR 게이트로 승격

사용자 제기 — doc-check.py가 정규식으로 결정론적 판정을 하는데 표기가 흔들리면
문제가 커지니, 정규식을 늘리기보다 "예상 가능 범위"를 컨벤션으로 좁히는 게 싸지
않냐. 실측해보니 날짜 표기는 이미 100% 균일해서 고칠 게 없었고(강제 장치 없이),
드리프트는 절 인용 쪽이었다 — WARN 86건 중 78건(91%)이 절 참조 불일치.

핵심은 그 78건이 코퍼스가 지저분한 게 아니라 **검사기가 못 읽는 것**이었다는
점이다. 이 코퍼스는 `**볼드**` 줄을 하위 절로 쓰는데 headings()가 `#`만 봤다.

## 동작 변화 (문서 정정으로만 보이지만 게이트가 바뀐다)

- 절 참조 불일치가 **WARN → ERROR**. 이제 절 인용 오류가 커밋을 막는다.
- 절 인식이 `#` 헤딩 + `**볼드**` 절로 확장. 단 볼드는 **빈 줄 다음이나
  리스트 항목 머리**만 인정 — 문단이 줄바꿈되며 우연히 줄머리에 걸린 강조를
  절로 오인하던 걸 커밋 전 감사가 잡아 조였다.
- 인용 길이 상한 60→160자. 60자를 넘으면 매칭 자체가 안 걸려 검사에서
  **조용히** 빠져나갔음(위양성보다 나쁜 구멍).
- 비교를 공백 무시로(줄바꿈 인용 대응), 선두 장식·상태/날짜 태그 정규화,
  `initreq/` 대상 인용은 절 검사 면제(읽기 전용 외부 원본).

## 규약

`conventions.md`에 "문서 표기 규약" 절 신설 — 절 인용 규약(의역 금지, 헤딩은
부분문자열/볼드는 앞부분일치, 태그로 닫히는 볼드 캐비엇, blockquote 함정),
세션은 산문 서수 말고 파일 ID로 지칭. 날짜 마커 라벨 어휘 닫기는 사용자 판단
으로 기각(기계 검사 대상이 아니라 읽는 쪽 판단 재료).

## 결과

절 참조 불일치 78 → 0. 36건은 검사기 수정으로 사라졌고(애초에 위양성), 42건은
인용을 실제 절 제목으로 손으로 고쳤다. 마지막 15건은 서브에이전트 3개에 병렬
위임해 추적 — **설계 서술이 유실된 건은 0건**, 대부분 코드 주석·본문 산문·
주제명처럼 애초에 절이 아닌 걸 절로 인용해온 것이었다.

부수로 드러나 같이 고친 것: onchange-plan이 9차 분할 때 일부러 안 옮긴 절을
잘못된 파일로 가리키던 것, brand-plan이 이미 이행된 정정을 "정정 대상"이라
부르던 것, ROADMAP의 blockquote가 인용 줄바꿈 때문에 깨져 있던 것,
pre-implementation-audit의 해소된 항목이 "아직 안 고침" 절에 남아 있던 것
(사용자 결정으로 "이미 고침"으로 이동).

커밋 전 감사 4라운드(에이전트 8개)를 돌렸고, 발견 추이는 2→2→1→0이다.
매 라운드 발견이 "직전 라운드 수정이 만든 새 결함"이었던 게 특징 — 규약을
세우는 커밋이 그 규약의 첫 위반자가 된다는 걸 실측으로 확인했다. 상세는
.claude/session/2026-08-16-03-doc-check-section-convention.md.

부수: __pycache__를 .gitignore에 추가하고 추적 해제(32e9db0에 실수로 딸려
들어가 있었음). quad-doc-auditor에 작업 트리를 바꾸는 git 명령 금지 규약 추가
— 감사자가 git stash를 걸어 메인 세션 스테이지가 반복적으로 풀렸다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zk7XHkSfiBfdPLZUQHdZf
This commit is contained in:
qwreey 2026-08-16 10:46:30 +09:00
parent d23d46f746
commit 40a5daf694
Signed by: qwreey
GPG key ID: D28DB79297A214BD
33 changed files with 615 additions and 83 deletions

View file

@ -29,7 +29,7 @@
| `feedback/` | 실사용 피드백을 정리한 긴 로그 — **[2026-08-16 기준] 폴더 자체가 아직 없음**(구현 시작 전, 첫 피드백이 생길 때 만들면 됨). `qa-request/`는 빈 폴더로 존재하지만 여긴 그것도 아직 아님 | | `feedback/` | 실사용 피드백을 정리한 긴 로그 — **[2026-08-16 기준] 폴더 자체가 아직 없음**(구현 시작 전, 첫 피드백이 생길 때 만들면 됨). `qa-request/`는 빈 폴더로 존재하지만 여긴 그것도 아직 아님 |
| `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`**비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` | | `luau-test/` | **[2026-08-09 신설]** `base/` 확정 사항 중 "추론만으로 확정하고 실제 Luau로 부딪혀본 적 없는 것"(M0 스파이크 대상)을 `luau`/`luau-analyze`/`luau-lsp`/Roblox Studio로 사용자가 직접 돌려볼 독립 실행 스크립트 모음. **[2026-08-13 여섯 번째 세션, 첫 실측]** `luau`/`luau-analyze` 바이너리가 생겨 처음으로 실제 실행 — **런타임 12개 전원 통과**, 타입 쪽에서 `:Compute(fn)` lazy 핸들 계약이 Luau 추론과 충돌하는 게 드러남(당시 `question.md` 0-Y). **[2026-08-13 열세 번째 세션]** 그 0-Y가 해소되며 `review-required/`**비었음** — 계약은 유지 확정, 남은 건 Luau 자체 한계라 `base/typing-limits.md`가 담당. **`STATUS.md`가 상태의 소스**(pass / 사람 결정 필요 / 스파이크 깨짐 / 미실행 분류 — 사람이 먼저 볼 것만 위에), `luau-test/README.md`는 각 파일의 검증 의도·배경, 실행 결과 상세는 `audit/luau-test-first-run-2026-08-13.md` |
| `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김. **개수는 폴더가 소스**(여기서 세지 않음): `luau-test-first-run-2026-08-13.md`(첫 실측 라운드 전체 — 런타임 12개 통과, 구 `question.md` 0-Y의 1차 근거. **단 이 문서의 "콜백이 raw 값을 받으면 완전 클린" 판정은 아래 `type-recursion-issue/`가 뒤집었음**), `gcconn-trick-verification.md`(사용자가 Studio에서 직접 돌린 gcconn 트릭 부분 확인 — `10`의 A 섹션 앞부분만. **[2026-08-14 다섯 번째 세션, 열한 번째 세션에 `canBound` 재도입 반영해 재갱신]** 실측된 사실 자체는 그대로 유효하고 `value` 단독 1-인자 재정정으로 오히려 더 중요해졌음 — 이중 바인딩 게이트(`canBound`)/emit 게이팅(`canExecute`)/재바인딩 허용/`value` 쪽 복사 gcconn 판정/Instance userdata 동일성/B/C가 미확인), **`type-recursion-issue/`**(**[2026-08-13 열세 번째 세션 신설]** 0-Y 재실측 전체 — `REPORT.md` + `spikes/`(개수는 폴더가 소스). 다른 audit 기록과 달리 **스크립트를 같이 둠**: 이 건의 근거가 "여러 formulation을 서로 대조한 것"이라 개별 파일을 직접 돌려야 판정이 재현되기 때문. 결론은 `base/typing-limits.md`로 승격됨), `fallback-xpcall-verification.md`(**[2026-08-14 신설]** `base/fallback-plan.md``Traceback` 메커니즘 전부 확인 — 클로저 업밸류 배선/중첩 스택 캡처/`err: any`/`error(msg)` 위치 접두 10개 검증 전부 통과. 스크립트 1개뿐이라 재현용으로 같이 둠: `fallback-xpcall-spike.luau`), **`type-recursive-issue-with-typeof/`**(**[2026-08-15 신설]** 사용자가 발견한 `typeof(named fn)` 간접참조가 0-Y(재귀 제네릭 반환 leak)를 실제로 우회하는지 실측 — `REPORT.md` + `spikes/`. 결론: 인라인 대신 이름 붙은 함수 + `typeof`로 선언하면 LHS 명시 없이도 다운스트림이 안전해짐(체이닝 50단·타입 변경·중첩 self 호출까지 확인), `typing-limits.md` §1 ③으로 승격. 부수적으로 `setmetatable` 확장 시도에서 quad와 무관한 Luau 0.733 솔버 버그(모순 진단 두 개 동시 발생) 발견, 채택 안 함. `luau-test/16`(type function으로 `Store<T>` 레코드 필드 합성) 복구도 이 조사 중 완료 — API 버전 드리프트였을 뿐 설계 문제 아니었음, `typing-limits.md` §5 승격), **`type-recursive-issue-try-callback/`**(**[2026-08-15 신설]** 콜백 파라미터 무주석 추론을 뚫을 방법이 정말 없는지 type function/메타테이블/오버로드/제네릭 디폴트 등 전방위로 재시도 — `REPORT.md` + `spikes/`(개수는 폴더가 소스 — 최초 라운드 + `/code-review high`가 이중 꺾쇠 명시적 제네릭 인스턴스화를 안 시도했음을 지적해 추가된 후속 조사 라운드로 구성). 결론: quad의 `state:Compute(fn)` 단일 호출 모양을 유지한 채로는 여전히 안 됨. 발견 셋 — (1) 근본 원인이 재귀 자기참조가 아니라 "제네릭 콜백 인자 전반에 컨텍스트 타입 전파가 안 됨"이라는 게 더 정확함(재귀 없는 최소 사례로도 재현), (2) `T`를 명시 중간 변수로 먼저 고정하거나 재사용 가능한 monomorphize 헬퍼를 거치면 실제로 추론이 살아나지만 둘 다 단일 콜론 호출을 2단계 체인으로 바꿔야만 해서 §0 대전제로 채택 안 함, (3) 이중 꺾쇠 명시 인스턴스화(`Compute<<T,U>>(fn)`)는 leaf 호출에선 sound하게 성립하지만(spurious 진단 원인도 규명 — read-only/read-write 가변성 불일치) 매 호출 T/U 전부 명시 필요 + 중첩 self 호출 여전히 실패라 순손해로 채택 안 함) | | `audit/` | **[2026-08-13 신설]** `luau-test/` 등 스파이크를 실제로 돌려본 뒤 "무엇이 확인됐고 무엇이 아직 안 됐는지"를 기록하는 곳 — 스크립트/계획 자체가 아니라 **실측 결과**만 다룸. base/luau-test와 달리 부분 확인(일부만 통과)도 있는 그대로 기록, 완전히 해소되면 관련 `base/`/`luau-test/README.md` 캐비엇을 지우고 이 문서는 근거로 남김. **개수는 폴더가 소스**(여기서 세지 않음): `luau-test-first-run-2026-08-13.md`(첫 실측 라운드 전체 — 런타임 12개 통과, 구 `question.md` 0-Y의 1차 근거. **단 이 문서의 "콜백이 raw 값을 받으면 완전 클린" 판정은 아래 `type-recursion-issue/`가 뒤집었음**), `gcconn-trick-verification.md`(사용자가 Studio에서 직접 돌린 gcconn 트릭 부분 확인 — `10`의 A 섹션 앞부분만. **[2026-08-14 다섯 번째 세션, 열한 번째 세션에 `canBound` 재도입 반영해 재갱신]** 실측된 사실 자체는 그대로 유효하고 `value` 단독 1-인자 재정정으로 오히려 더 중요해졌음 — 이중 바인딩 게이트(`canBound`)/emit 게이팅(`canExecute`)/재바인딩 허용/`value` 쪽 복사 gcconn 판정/Instance userdata 동일성/B/C가 미확인), **`type-recursion-issue/`**(**[2026-08-13 열세 번째 세션 신설]** 0-Y 재실측 전체 — `REPORT.md` + `spikes/`(개수는 폴더가 소스). 다른 audit 기록과 달리 **스크립트를 같이 둠**: 이 건의 근거가 "여러 formulation을 서로 대조한 것"이라 개별 파일을 직접 돌려야 판정이 재현되기 때문. 결론은 `base/typing-limits.md`로 승격됨), `fallback-xpcall-verification.md`(**[2026-08-14 신설]** `base/fallback-plan.md``Traceback` 메커니즘 전부 확인 — 클로저 업밸류 배선/중첩 스택 캡처/`err: any`/`error(msg)` 위치 접두 10개 검증 전부 통과. 스크립트 1개뿐이라 재현용으로 같이 둠: `fallback-xpcall-spike.luau`), **`type-recursive-issue-with-typeof/`**(**[2026-08-15 신설]** 사용자가 발견한 `typeof(named fn)` 간접참조가 0-Y(재귀 제네릭 반환 leak)를 실제로 우회하는지 실측 — `REPORT.md` + `spikes/`. 결론: 인라인 대신 이름 붙은 함수 + `typeof`로 선언하면 LHS 명시 없이도 다운스트림이 안전해짐(체이닝 50단·타입 변경·중첩 self 호출까지 확인), `typing-limits.md` §1 ③으로 승격. 부수적으로 `setmetatable` 확장 시도에서 quad와 무관한 Luau 0.733 솔버 버그(모순 진단 두 개 동시 발생) 발견, 채택 안 함. `luau-test/16`(type function으로 `Store<T>` 레코드 필드 합성) 복구도 이 조사 중 완료 — API 버전 드리프트였을 뿐 설계 문제 아니었음, `typing-limits.md` §5 승격), **`type-recursive-issue-try-callback/`**(**[2026-08-15 신설]** 콜백 파라미터 무주석 추론을 뚫을 방법이 정말 없는지 type function/메타테이블/오버로드/제네릭 디폴트 등 전방위로 재시도 — `REPORT.md` + `spikes/`(개수는 폴더가 소스 — 최초 라운드 + `/code-review high`가 이중 꺾쇠 명시적 제네릭 인스턴스화를 안 시도했음을 지적해 추가된 후속 조사 라운드로 구성). 결론: quad의 `state:Compute(fn)` 단일 호출 모양을 유지한 채로는 여전히 안 됨. 발견 셋 — (1) 근본 원인이 재귀 자기참조가 아니라 "제네릭 콜백 인자 전반에 컨텍스트 타입 전파가 안 됨"이라는 게 더 정확함(재귀 없는 최소 사례로도 재현), (2) `T`를 명시 중간 변수로 먼저 고정하거나 재사용 가능한 monomorphize 헬퍼를 거치면 실제로 추론이 살아나지만 둘 다 단일 콜론 호출을 2단계 체인으로 바꿔야만 해서 §0 대전제로 채택 안 함, (3) 이중 꺾쇠 명시 인스턴스화(`Compute<<T,U>>(fn)`)는 leaf 호출에선 sound하게 성립하지만(spurious 진단 원인도 규명 — read-only/read-write 가변성 불일치) 매 호출 T/U 전부 명시 필요 + 중첩 self 호출 여전히 실패라 순손해로 채택 안 함) |
| `tools/` | **[2026-08-13 아홉 번째 세션 신설]** 코퍼스 기계 점검 — `doc-check.py`가 깨진 파일/절 참조, README 색인 누락, 날짜 없는 시한부 주장("아직 안 돌려봄" 등), 미반영 ⚠️ 배너를 한 번에 훑음. **중대 변경 후 커밋 전에 돌릴 것**(`python3 .claude/tools/doc-check.py`) — 수동 감사에서 나온 발견의 대부분이 이 종류였고, 실제로 문서를 쪼개다 잘못 옮긴 참조를 이게 잡아냄. ERROR는 고치고 WARN은 판단 대상 | | `tools/` | **[2026-08-13 신설, `session/2026-08-13-09-structure-and-guardrails.md`]** 코퍼스 기계 점검 — `doc-check.py`가 깨진 파일/절 참조, README 색인 누락, 날짜 없는 시한부 주장("아직 안 돌려봄" 등), 미반영 ⚠️ 배너를 한 번에 훑음. **[2026-08-16]** 절 참조는 WARN이 아니라 **ERROR** — 판정 규칙은 `conventions.md`의 "절 인용 규약"이 소스. **중대 변경 후 커밋 전에 돌릴 것**(`python3 .claude/tools/doc-check.py`) — 수동 감사에서 나온 발견의 대부분이 이 종류였고, 실제로 문서를 쪼개다 잘못 옮긴 참조를 이게 잡아냄. ERROR는 고치고 WARN은 판단 대상 |
| `agents/` | **[2026-08-16 신설]** 프로젝트 서브에이전트 정의(`.claude/agents/*.md`, Claude Code 표준 위치). 현재 `quad-doc-auditor.md` 하나 — `doc-check.py`가 못 잡는 의미론적 stale/모순(본문 문장이 뒤집힌 결정을 여전히 서술, 개수/목록 이중 소스 드리프트 등)을 신선한 맥락에서 찾는 읽기 전용 감사자. 중대 변경 커밋 전에 위임하는 게 기본 — **여러 개를 병렬로 돌리고 수정은 메인 세션이 일괄로 하는 루프**(패스 수는 여기 안 적음 — 소스는 conventions.md)이며 절차는 `.claude/conventions.md` "작업 방식" 절이 소스(**[2026-08-16]** 이 루프를 담던 `workflows/quad-handover-audit.js`는 토큰 과다·픽스 에이전트발 부정확 서술·사용자 질의 불가 때문에 폐기됨). `tools/`의 기계 점검과 짝을 이루는 의미론적 점검 계층 | | `agents/` | **[2026-08-16 신설]** 프로젝트 서브에이전트 정의(`.claude/agents/*.md`, Claude Code 표준 위치). 현재 `quad-doc-auditor.md` 하나 — `doc-check.py`가 못 잡는 의미론적 stale/모순(본문 문장이 뒤집힌 결정을 여전히 서술, 개수/목록 이중 소스 드리프트 등)을 신선한 맥락에서 찾는 읽기 전용 감사자. 중대 변경 커밋 전에 위임하는 게 기본 — **여러 개를 병렬로 돌리고 수정은 메인 세션이 일괄로 하는 루프**(패스 수는 여기 안 적음 — 소스는 conventions.md)이며 절차는 `.claude/conventions.md` "작업 방식" 절이 소스(**[2026-08-16]** 이 루프를 담던 `workflows/quad-handover-audit.js`는 토큰 과다·픽스 에이전트발 부정확 서술·사용자 질의 불가 때문에 폐기됨). `tools/`의 기계 점검과 짝을 이루는 의미론적 점검 계층 |
| `agent-memory/` | **[2026-08-16 신설]** 서브에이전트가 라운드를 넘겨 유지하는 영속 메모리(`agent-memory/<에이전트 이름>/MEMORY.md`가 색인). 지금은 `quad-doc-auditor/` 하나 — 코퍼스 구조, 반복되는 실패 패턴 등을 기억해 감사 라운드마다 처음부터 파악하지 않게 함. **사람이 손으로 채우는 문서가 아니라 에이전트가 스스로 쓰는 것**이지만, `.gitignore` 대상이 아니라 커밋하면 코퍼스 일부가 되고 `doc-check.py` 검사 대상에도 들어감(감사 대상이기도 하다는 뜻 — 여기 적힌 주장도 stale해질 수 있음). **[2026-08-16 확정] 커밋해서 추적함**(사용자 결정). 사용자 논거: "실 기록이고 디펜던시도 아니고, 어차피 `SAFETY.md`에 따라 구현 시점에는 컨테이너에서 개발되며 다른 프라이빗 git에 올라가고 검토 후 머징되는거라, 문제되는 메모리 있으면(환경 노출 등) 사람이 감사처리 마지막으로 함. 결국 프로젝트 사이드 기록이고 같이 올려지는게 맞는게, 개발 환경이 다수라서 필요해보임" — 즉 **개발 환경이 여러 개라 메모리가 따라다녀야 하고**, 노출 위험은 머지 전 사람 검토가 최종 방어선. 커밋하는 쪽이 정해졌으니 여기 내용도 감사 대상이다(에이전트가 자기 메모리에 stale한 결론을 남기는 일이 실제로 있었음 — 2026-08-16에 폐기된 워크플로를 살아있는 것처럼 서술한 2건이 감사로 잡힘) | | `agent-memory/` | **[2026-08-16 신설]** 서브에이전트가 라운드를 넘겨 유지하는 영속 메모리(`agent-memory/<에이전트 이름>/MEMORY.md`가 색인). 지금은 `quad-doc-auditor/` 하나 — 코퍼스 구조, 반복되는 실패 패턴 등을 기억해 감사 라운드마다 처음부터 파악하지 않게 함. **사람이 손으로 채우는 문서가 아니라 에이전트가 스스로 쓰는 것**이지만, `.gitignore` 대상이 아니라 커밋하면 코퍼스 일부가 되고 `doc-check.py` 검사 대상에도 들어감(감사 대상이기도 하다는 뜻 — 여기 적힌 주장도 stale해질 수 있음). **[2026-08-16 확정] 커밋해서 추적함**(사용자 결정). 사용자 논거: "실 기록이고 디펜던시도 아니고, 어차피 `SAFETY.md`에 따라 구현 시점에는 컨테이너에서 개발되며 다른 프라이빗 git에 올라가고 검토 후 머징되는거라, 문제되는 메모리 있으면(환경 노출 등) 사람이 감사처리 마지막으로 함. 결국 프로젝트 사이드 기록이고 같이 올려지는게 맞는게, 개발 환경이 다수라서 필요해보임" — 즉 **개발 환경이 여러 개라 메모리가 따라다녀야 하고**, 노출 위험은 머지 전 사람 검토가 최종 방어선. 커밋하는 쪽이 정해졌으니 여기 내용도 감사 대상이다(에이전트가 자기 메모리에 stale한 결론을 남기는 일이 실제로 있었음 — 2026-08-16에 폐기된 워크플로를 살아있는 것처럼 서술한 2건이 감사로 잡힘) |
| `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, `.claude/session-summary.md`의 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 | | `session/` | **[2026-08-11 신설]** 세션별 상세 로그 원문(시행착오·정정 전 서술 포함, `quadnomicon` 개발로그 소재용) — 루트 `CLAUDE.md`가 3196줄까지 불어나 성능 저하를 유발해서 분리함. 파일명 `YYYY-MM-DD-NN-slug.md`, `.claude/session-summary.md`의 각 항목이 여기로 링크. 항상 읽을 필요 없음 — 결정의 논의 과정이 궁금할 때만 |
@ -66,7 +66,7 @@
| `event-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리 — 사용자가 직접 지목]** 이벤트 바인딩 — 핸들러가 self(Instance)를 **안** 받는다는 확정(Ref가 이미 커버, 이중 쓰기 경로 방지), 이벤트도 store-bind 가능하며 `false`를 넣으면 disconnect. 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 `bind-system-plan.md`에 남음, `GetPropertyChangedSignal``onchange-plan.md`. **분리는 순수 이동** | | `event-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리 — 사용자가 직접 지목]** 이벤트 바인딩 — 핸들러가 self(Instance)를 **안** 받는다는 확정(Ref가 이미 커버, 이중 쓰기 경로 방지), 이벤트도 store-bind 가능하며 `false`를 넣으면 disconnect. 이벤트 *네이밍* 관례는 인스턴스 생성과 한 절에 섞여 있어 `bind-system-plan.md`에 남음, `GetPropertyChangedSignal``onchange-plan.md`. **분리는 순수 이동** |
| `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 branded 타입 전부로 일반화(`isPostRef` 포함, 2026-08-14 아홉 번째 세션). 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** | | `brand-plan.md` | **[2026-08-13 아홉 번째 세션, `bind-system-plan.md`에서 분리]** `Brand` — 런타임 nominal 타입 판별 통합 메커니즘(`Brand.set`/`Brand.get`), `isState`를 branded 타입 전부로 일반화(`isPostRef` 포함, 2026-08-14 아홉 번째 세션). 동작/구현은 확정, **이름 `Brand` 자체만 용어 정리 대기**(`question.md` 1번). **분리는 순수 이동** |
| `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)``Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) | | `tween-plan.md` | **[2026-08-12 세션, `research/`에서 승격]** 값-레벨 `Tween<T>` 래퍼(PropertyHandler가 소비, 구 특수 bind key 모델은 `archive/tween-special-bind-key-reversed.md`). 3-상태 릴레이션 슬롯(`{Tween,Value}\|true\|nil`), `T'=T\|Tween<T>` 타입 치환. 옵션 값 모양은 `Info: TweenInfo?` 우선+편의 필드 폴백, override는 `Tween.Cancel`(기본)/`Tween.Finish` 2값. `Animate(info)``Tween` opts를 `T\|State<T>`로 받아 `:Apply`로 꽂는 sugar. 자연완료 시 per-instance 북키핑은 정리 안 해도 됨으로 확정(목표값 도달 상태라 부작용 없음, Completed 이벤트 구독 장치는 오버엔지니어링으로 판단). `initValue`는 사용자가 직접 처리(에이전트 범위 제외) |
| `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary빈 자리 아님" 결론 위에 얹힘). `Fallback``pcall` 기반(trace 없음), `Traceback``xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 | | `fallback-plan.md` | **[2026-08-14 세션, `research/`에서 승격]** `Fallback`/`Traceback` — 컴포넌트 함수를 감싸 에러 시 플레이스홀더를 그려주는 순수 슈가(`additional-primitives-plan.md`의 "Error Boundary" 절이 내린 "빈 자리 아님" 결론 위에 얹힘). `Fallback``pcall` 기반(trace 없음), `Traceback``xpcall`+`debug.traceback` 기반(trace 항상 있음) — 플래그 대신 별도 함수로 분리(`Ref`/`PreRef`와 같은 패턴). `err: any`(Lua `error()`가 임의 값을 던질 수 있음, `error(msg)` 기본 호출의 위치 접두 캐비엇 포함) 확정. 패키지는 `quad-base`, 이름 확정. 메커니즘 실측은 `audit/fallback-xpcall-verification.md`. 구현 우선순위는 형제 백로그(`quad-mock`/`quad-debug`/`Operator`)와 동급, 맨 뒤 |
| `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. **[2026-08-14 열 번째 세션]** `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고 `Observer`/`Effect`는 제외되는 쪽으로 확정되며 `OnDestroyed` 이름 재검토 조건이 발동 없이 종결 — `OnDestroyed`가 최종 이름, 용어 대기열에서도 제외 | | `lifecycle-hooks-plan.md` | **[2026-08-14 아홉 번째 세션, `research/`에서 승격]** 생명주기 훅 슈가 `OnCreated`/`OnRendered`/`OnDestroyed` — 각각 `PreRef():Callback(fn)`/`PostRef():Callback(fn)`/`Effect(function() return fn end)`를 반환하는 **순수 팩토리 함수**라 새 타입/Dispatch 개념이 전혀 안 생김(호출 즉시 평가돼 기존 인스턴스로 사라짐), 여러 개 나란히 등록도 자연 지원(단 **같은 계열끼리의 순서는 미보장**). 마지막 열린 항목이던 `OnRendered`는 사용자가 **채택 확정** — 메커니즘은 `PostRef`(`base/ref-plan.md`), 원래 열어뒀던 (a)/(b)/(c) 중 **(a)**. 캐비엇: `OnRendered`는 서브트리 완성은 보장하지만 **이 인스턴스가 부모에 붙기 전**에 불림(React `componentDidMount`와 다름) — 문서화 필수. 패키지 `quad-base` 확정. **[2026-08-14 열 번째 세션]** `dispose()` 범위(0-B)가 `Slot`+`Instance`로 좁혀지고 `Observer`/`Effect`는 제외되는 쪽으로 확정되며 `OnDestroyed` 이름 재검토 조건이 발동 없이 종결 — `OnDestroyed`가 최종 이름, 용어 대기열에서도 제외 |
## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설) ## `reference/` — 온디맨드 참고 자료 (2026-08-07 신설)

View file

@ -18,7 +18,7 @@ CLAUDE.md 실제 43줄인데 "39줄"이라 적고, session-summary.md 실제 127
숫자가 설명하는 파일이 최종 커밋되는 시점 사이에 다른 편집(체크리스트 숫자가 설명하는 파일이 최종 커밋되는 시점 사이에 다른 편집(체크리스트
항목 추가, 배너 삽입 등)이 끼어들기 쉬움 — 자기 자신을 설명하는 숫자는 항목 추가, 배너 삽입 등)이 끼어들기 쉬움 — 자기 자신을 설명하는 숫자는
구조적으로 닭-달걀 문제라 사람이 아무리 조심해도 어긋난다. 구조적으로 닭-달걀 문제라 사람이 아무리 조심해도 어긋난다.
`conventions.md`의 "개수·목록·상태는 소스 하나만" 원칙이 일반적으로는 `conventions.md`의 "개수·목록·상태는 소스 하나만 둘 것" 원칙이 일반적으로는
"여러 문서에 같은 값을 반복 적지 말 것"을 말하지만, 이 변형(자기 자신을 "여러 문서에 같은 값을 반복 적지 말 것"을 말하지만, 이 변형(자기 자신을
설명하는 숫자)은 그 원칙조차 지켜도(값이 파일들 사이에 일치해도) 실제 설명하는 숫자)은 그 원칙조차 지켜도(값이 파일들 사이에 일치해도) 실제
파일 상태와는 다를 수 있다는 게 요점 — 일치 여부가 아니라 **애초에 파일 상태와는 다를 수 있다는 게 요점 — 일치 여부가 아니라 **애초에

View file

@ -108,6 +108,18 @@ model: sonnet
- 설계 자체의 옳고 그름(quad 아키텍처가 맞는 선택인지)은 네 스코프가 - 설계 자체의 옳고 그름(quad 아키텍처가 맞는 선택인지)은 네 스코프가
아니다. 오직 "코퍼스가 스스로와 모순되지 않는가"만 본다. 아니다. 오직 "코퍼스가 스스로와 모순되지 않는가"만 본다.
## ⛔ 작업 트리를 바꾸는 git 명령 금지 (2026-08-16 신설)
**`git stash`(및 `checkout`/`restore`/`reset`/`rm`/`add` 등 인덱스나 워킹트리를
바꾸는 모든 명령)를 쓰지 마라.** 너는 보통 **커밋 안 된 작업이 올라가 있는
트리**에서 돌고, 그 상태에서 stash를 걸면 메인 세션의 작업을 통째로 날릴 수
있다. 2026-08-16 세션에서 실제로 여러 감사자가 HEAD와 대조하려고 `git stash`
써서 메인 세션이 스테이지해둔 변경이 반복적으로 되돌아갔다(다행히 유실은 없었음).
HEAD 시점 내용이 필요하면 트리를 건드리지 않는 방법을 써라 —
`git show HEAD:<경로>`, `git diff HEAD -- <경로>`, `git log -S`,
`git cat-file`. 읽기 전용은 도구 목록이 아니라 이 행동 규약으로 지킨다.
## 출력 형식 ## 출력 형식
발견마다: `파일:줄` — 무슨 문장이 무엇과 모순/stale인지 한 문장 — 어떻게 발견마다: `파일:줄` — 무슨 문장이 무엇과 모순/stale인지 한 문장 — 어떻게

View file

@ -55,7 +55,7 @@ Studio에서 실행된 사용자 자작 스크립트(공식 `10` 파일이 아
조건 1(신호 발화)은 회피 확인. 조건 1(신호 발화)은 회피 확인.
2. **연결이 살아있는 동안 콜백 클로저가 캡처한 값이 GC 안 됨** — Test 1/2 2. **연결이 살아있는 동안 콜백 클로저가 캡처한 값이 GC 안 됨** — Test 1/2
둘 다 `weak[1]`이 6 epoch 내내 살아있음. `lifecycle-pattern.md` 둘 다 `weak[1]`이 6 epoch 내내 살아있음. `lifecycle-pattern.md`
"클로저 생존이 곧 gchold 생존" 주장과 일치. **[2026-08-14 세 번째 "gcconn/gchold는 Instance 생성 시점에 만든다" 주장과 일치. **[2026-08-14 세 번째
세션]** 이 스크립트가 실제로 업밸류로 캡처한 값이 `target`(=Instance) 세션]** 이 스크립트가 실제로 업밸류로 캡처한 값이 `target`(=Instance)
자체였다는 점에서, 새 모델이 요구하는 **"클로저가 `gchold`뿐 아니라 자체였다는 점에서, 새 모델이 요구하는 **"클로저가 `gchold`뿐 아니라
`inst`까지 캡처해 userdata 동일성을 고정한다"**는 조치의 전반부도 같이 `inst`까지 캡처해 userdata 동일성을 고정한다"**는 조치의 전반부도 같이

View file

@ -176,8 +176,8 @@ API 전부에 걸림. 2026-08-07 일곱 번째 세션의 커링 스타일 확정
## 그 외 — 스파이크 코드 결함이었던 것들 (전부 수정 완료) ## 그 외 — 스파이크 코드 결함이었던 것들 (전부 수정 완료)
- **`17` 크래시의 원인이 실은 문서 결함이었음** — `modifier-plan.md` - **`17` 크래시의 원인이 실은 문서 결함이었음** — `modifier-plan.md` `내부 구현`
"데이터를 테이블에 직접 두고"가 "self 최상위 리터럴 키"로 읽힐 여지가 본문에 있던 "데이터를 테이블에 직접 두고"가 "self 최상위 리터럴 키"로 읽힐 여지가
있었는데, 그렇게 하면 `__index``rawget` 성공 시 안 불리므로 **같은 있었는데, 그렇게 하면 `__index``rawget` 성공 시 안 불리므로 **같은
필드를 두 번째로 변환 함수와 함께 호출하는 순간 죽음**(`attempt to call 필드를 두 번째로 변환 함수와 함께 호출하는 순간 죽음**(`attempt to call
a number value`). 그 재호출 패턴이 바로 문서 3·4번 절의 대표 용례라 a number value`). 그 재호출 패턴이 바로 문서 3·4번 절의 대표 용례라

View file

@ -51,8 +51,8 @@ Ref(직접 참조 획득) 쪽으로 빠지는 게 맞고, Attribute는 프리미
대안(테이블/참조를 담는 용도)으로 나온 배경이지만, 지금은 Roblox Attribute가 대안(테이블/참조를 담는 용도)으로 나온 배경이지만, 지금은 Roblox Attribute가
Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Attribute를 Instance 참조 타입도 지원해서 `ObjectValue` 없이도 Ref 용도로 Attribute를
그대로 쓸 수 있다는 점을 사용자가 짚음(`research/debug-tooling-plan.md`의 그대로 쓸 수 있다는 점을 사용자가 짚음(`research/debug-tooling-plan.md`의
"Value 오브젝트 기각, Attribute로 확정" 결정과 같은 방향 — Instance 타입 "Value 오브젝트(StringValue/ObjectValue 등)는 기각" 결정과 같은 방향
지원까지 감안하면 그 결정의 근거가 한층 더 탄탄해짐). — Instance 타입 지원까지 감안하면 그 결정의 근거가 한층 더 탄탄해짐).
**확정(2026-08-09 열한 번째 세션) — 둘 다 채택**: **확정(2026-08-09 열한 번째 세션) — 둘 다 채택**:
- `[AttributeKey<<boolean>> "name"] = true` (리터럴 또는 store-bind 값) — - `[AttributeKey<<boolean>> "name"] = true` (리터럴 또는 store-bind 값) —
@ -436,8 +436,7 @@ quad-roblox** 소속이었음 — 그런데 실제로 엔진에 종속된 건
(`inst:SetAttribute`)뿐이고, 이름 claim·그룹 위임·`None` 처리·이름별 weak (`inst:SetAttribute`)뿐이고, 이름 claim·그룹 위임·`None` 처리·이름별 weak
캐시는 전부 순수 부기임. 웹에도 대응물이 있으므로(`data-*`) 그 배치대로면 캐시는 전부 순수 부기임. 웹에도 대응물이 있으므로(`data-*`) 그 배치대로면
**같은 소유권 알고리즘을 백엔드마다 재구현**하게 됨 — `architecture.md` **같은 소유권 알고리즘을 백엔드마다 재구현**하게 됨 — `architecture.md`
"엔진마다 큰 구현을 중복하지 않기 위해 디스패치 엔진을 base가 인터페이스로 "패키지 경계" 절이 세운 원칙에 정면으로 어긋남.
소유한다"는 원칙에 정면으로 어긋남.
**확정된 배치**: **확정된 배치**:

View file

@ -104,7 +104,9 @@ RobloxFactory(QuadBase)` 세 줄 정도로 직접 조립하면 됨(별도 번들
**확정(2026-08-04 3차 라운드)**: `RobloxFactory`를 같은 `BaseModule`에 여러 **확정(2026-08-04 3차 라운드)**: `RobloxFactory`를 같은 `BaseModule`에 여러
번 호출했을 때 — **같은 팩토리로 재호출하면 무시(no-op)**, hot-reload처럼 번 호출했을 때 — **같은 팩토리로 재호출하면 무시(no-op)**, hot-reload처럼
초기화 스크립트가 다시 도는 경우를 안전하게 만듦. **다른 팩토리 초기화 스크립트가 다시 도는 경우를 안전하게 만듦. **다른 팩토리
(`AnotherFactory` 등, 가상의 예)로 재호출하면 에러** — 이건 `base/module-lifecycle-plan.md`의 "bind는 유일 슬롯" 원칙(이미 구현체가 있는데 또 (`AnotherFactory` 등, 가상의 예)로 재호출하면 에러** — 이건
`base/module-lifecycle-plan.md`의 "Bind는 누가, 어떻게 구현하는가" 절의
원칙(이미 구현체가 있는데 또
다른 구현체로 init하려 하면 오류)이 다루던 것과 정확히 같은 케이스, 이 다른 구현체로 init하려 하면 오류)이 다루던 것과 정확히 같은 케이스, 이
문서의 이전 "무시" 잠정안과 그 문서의 "오류" 잠정안이 서로 모순되는 게 문서의 이전 "무시" 잠정안과 그 문서의 "오류" 잠정안이 서로 모순되는 게
아니라 **같은 팩토리 재호출(무시) vs 다른 팩토리로 유일 슬롯 충돌(에러)이라는 아니라 **같은 팩토리 재호출(무시) vs 다른 팩토리로 유일 슬롯 충돌(에러)이라는

View file

@ -86,8 +86,8 @@ end
별도로 제공, `isState`는 여전히 `{State, Source}` 둘 다 통과시킴(상위 별도로 제공, `isState`는 여전히 `{State, Source}` 둘 다 통과시킴(상위
개념이니까 당연히). `component-composition-plan.md` 4번 절이 이미 개념이니까 당연히). `component-composition-plan.md` 4번 절이 이미
`isSource`가 존재한다고 가정하고 있었던 것과도 이걸로 정합됨(그동안 두 `isSource`가 존재한다고 가정하고 있었던 것과도 이걸로 정합됨(그동안 두
문서가 서로 모순돼 있었음). `base/modifier-plan.md`의 "별도 `isSource` 문서가 서로 모순돼 있었음). `base/modifier-plan.md`의 "`isState(x): boolean` 필요" 절에 있던 "별도 `isSource` 불필요" 서술은
불필요" 서술도 같이 정정 대상. `session/2026-08-07-08-none-sentinel-dispatch-brand.md`에서 이미 정정됨.
**갭 보강 — `isRef`/`isPreRef`/`isModifier`가 태그 목록에서 빠져있던 것 **갭 보강 — `isRef`/`isPreRef`/`isModifier`가 태그 목록에서 빠져있던 것
추가(2026-08-07 열 번째 세션), 이후 `isRef`/`isPreRef` 관계 자체가 추가(2026-08-07 열 번째 세션), 이후 `isRef`/`isPreRef` 관계 자체가
@ -121,7 +121,8 @@ end
`Brand.get(x) == SlotTag`/`Brand.get(x) == EffectTag`인 단순 항등 `Brand.get(x) == SlotTag`/`Brand.get(x) == EffectTag`인 단순 항등
predicate, 태그 자체는 원래부터 목록에 있었지만(`SlotTag`) `isX` predicate, 태그 자체는 원래부터 목록에 있었지만(`SlotTag`) `isX`
wrapper로 명시적으로 안 적혀 있던 것을 `base/modifier-plan.md` wrapper로 명시적으로 안 적혀 있던 것을 `base/modifier-plan.md`
"핸들러 계층 값이 필드로 들어오면 즉시 error" 절이 필요로 해서 이번에 "Modifier 필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이
들어오면 즉시 error" 절이 필요로 해서 이번에
같이 적음. 같이 적음.
**`None`은 이 레지스트리에 안 들어감 — 싱글턴이라 항등 비교로 충분.** **`None`은 이 레지스트리에 안 들어감 — 싱글턴이라 항등 비교로 충분.**

View file

@ -124,8 +124,9 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들
`typeof(v)`를 함께 출력하고, "quad-roblox 등 필요한 provider가 `typeof(v)`를 함께 출력하고, "quad-roblox 등 필요한 provider가
초기화됐는지 확인하라"는 안내만 덧붙임 — 그 이상의 특수 분기는 두지 초기화됐는지 확인하라"는 안내만 덧붙임 — 그 이상의 특수 분기는 두지
않음(다른 라이브러리에서도 흔한 "매치 실패=에러" 패턴 그대로). 않음(다른 라이브러리에서도 흔한 "매치 실패=에러" 패턴 그대로).
**이걸로 `module-lifecycle-plan.md`의 "provider가 아직 주입 안 된 **이걸로 `module-lifecycle-plan.md`의 "열린 질문이었던 것 — 전부
상태에서 dispatch가 호출되면?" 케이스(`pre-implementation-audit.md` 해소됨" 절에 있는 "provider가 아직 주입 안 된 상태에서 dispatch가
호출되면?" 케이스(`pre-implementation-audit.md`
1-4)도 별도 분기 없이 자동으로 해소됨** — provider 미주입 상태는 1-4)도 별도 분기 없이 자동으로 해소됨** — provider 미주입 상태는
결국 그 클래스를 다루는 핸들러가 레지스트리에 하나도 없는 상태이므로 결국 그 클래스를 다루는 핸들러가 레지스트리에 하나도 없는 상태이므로
"매치 실패"와 정확히 같은 경로로 수렴함. 오타 키/미지원 조합/provider "매치 실패"와 정확히 같은 경로로 수렴함. 오타 키/미지원 조합/provider
@ -207,7 +208,7 @@ retract 클로저를 반환하는 1-메소드 계약으로 합쳐짐 — 이 절
이전 처리를 무르는/멈추는 함수. **오직 "같은 key에 새 값이 들어와서 이전 처리를 무르는/멈추는 함수. **오직 "같은 key에 새 값이 들어와서
이전 처리를 갈아치우는" 시나리오에만 존재** — 인스턴스/바인드 전체가 이전 처리를 갈아치우는" 시나리오에만 존재** — 인스턴스/바인드 전체가
Destroy될 때는 이 클로저가 호출되지 않음(`base/lifecycle-pattern.md`의 Destroy될 때는 이 클로저가 호출되지 않음(`base/lifecycle-pattern.md`의
"quad는 라이프사이클 중간에 있지 않다" 원칙 참고). "quad는 자신이 만든 Instance의 라이프사이클" 절의 원칙 참고).
- 일반 프로퍼티는 애초에 "unset" 개념이 없음(`nil`로 셋하는 것도 그냥 셋 - 일반 프로퍼티는 애초에 "unset" 개념이 없음(`nil`로 셋하는 것도 그냥 셋
동작) — 그래서 프로퍼티 핸들러는 보통 no-op 클로저(`function() end`)만 동작) — 그래서 프로퍼티 핸들러는 보통 no-op 클로저(`function() end`)만
반환하면 됨. 반환하면 됨.
@ -526,8 +527,7 @@ end
키/그룹(이름 claim, 그룹→단일 키 위임, `None` 처리). 둘 다 웹에도 키/그룹(이름 claim, 그룹→단일 키 위임, `None` 처리). 둘 다 웹에도
대응물이 있고(`className`, `data-*`) 부기 로직이 엔진과 무관해서, 대응물이 있고(`className`, `data-*`) 부기 로직이 엔진과 무관해서,
백엔드마다 재구현하면 **같은 참조 카운트/소유권 알고리즘이 통째로 백엔드마다 재구현하면 **같은 참조 카운트/소유권 알고리즘이 통째로
복제**됨 — `architecture.md`의 "엔진마다 큰 구현을 중복하지 않기 위해 복제**됨 — `architecture.md`의 "패키지 경계" 절이 세운 원칙이 그대로 적용되는
디스패치 엔진을 base가 인터페이스로 소유한다"는 원칙이 그대로 적용되는
자리(2026-08-13 열네 번째 세션, 사용자 판단으로 재배치). **같은 패턴이 자리(2026-08-13 열네 번째 세션, 사용자 판단으로 재배치). **같은 패턴이
Dispatch 바깥에도 적용됨** — `dispose(value)`(`base/slot-plan.md`)는 Dispatch 바깥에도 적용됨** — `dispose(value)`(`base/slot-plan.md`)는
Dispatch 핸들러가 아니라 독립 탑레벨 유틸이지만, `isSlot`이 아닌 값은 Dispatch 핸들러가 아니라 독립 탑레벨 유틸이지만, `isSlot`이 아닌 값은

View file

@ -41,8 +41,8 @@ SyntheticEvent만 주는 것과 같은 모양).
무엇에 연결됐는가"를 선언된 반응형 그래프로 추적하는 것인데 무엇에 연결됐는가"를 선언된 반응형 그래프로 추적하는 것인데
(`research/debug-tooling-plan.md`), self로 얻은 Instance를 이벤트 (`research/debug-tooling-plan.md`), self로 얻은 Instance를 이벤트
핸들러 안에서 직접 mutate하는 경로는 그 그래프 밖 — `base/ 핸들러 안에서 직접 mutate하는 경로는 그 그래프 밖 — `base/
purity-and-effects-plan.md`의 "재사용 가능한 컴포넌트는 store만 purity-and-effects-plan.md`의 "결론: 입력받은 store만 처리하는
파라미터로 받아야 한다"는 이식성 원칙과도 같은 결. 함수가 좋은 컴포넌트" 절이 세운 이식성 원칙과도 같은 결.
4. **성능/GC**: self를 넘겨주려면 원본 콜백을 클로저로 한 번 더 감싸야 4. **성능/GC**: self를 넘겨주려면 원본 콜백을 클로저로 한 번 더 감싸야
함(`event:Connect(function(...) func(self, ...) end)`) — Connect마다 함(`event:Connect(function(...) func(self, ...) end)`) — Connect마다
불필요한 클로저 할당 비용이 들고, 최적화에도 GC 흐름에도 좋을 게 불필요한 클로저 할당 비용이 들고, 최적화에도 GC 흐름에도 좋을 게

View file

@ -108,8 +108,8 @@ Destroy되면 그 대상에 묶인 것들(Tween 등)도 자연히 죽은 상태
가능하면 GC에 맡김. 가능하면 GC에 맡김.
이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전 이 원칙 때문에 "값 교체 시 이전 처리를 무르는 것"(아래 `retract`)과 "완전
소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `research/ 소멸 시 정리"는 **하나로 통일** — 후자는 애초에 안 만듦. `base/tween-plan.md`
tween-plan.md`/`base/slot-plan.md`의 "cleanup" 표기는 대부분 `retract` `base/slot-plan.md`가 쓰던 용어 `cleanup` 표기는 대부분 `retract`
갱신됨(이름 변경 근거는 아래) — 잔여 표기 확인은 진행 중, 해당 문서들은 갱신됨(이름 변경 근거는 아래) — 잔여 표기 확인은 진행 중, 해당 문서들은
각자 별도로 정리될 예정. 각자 별도로 정리될 예정.
@ -505,8 +505,10 @@ Roblox 엔진 자체가 Destroy 시 Tag/Attribute/실행 중인 Tween을 전부
철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를 철회한다"는 의미로 가장 정확 — `process`/`retract` 쌍으로 자연스럽게 대구를
이룸.) 대부분의 문서에서 이 이름으로 갱신됨. 이룸.) 대부분의 문서에서 이 이름으로 갱신됨.
**[확인 완료, 2026-08-13 여섯 번째 세션] `base/effect-plan.md`의 "cleanup"은 **[확인 완료, 2026-08-13,
잔여 stale이 아니라 의도된 별개 개념** — 매 감사마다 재지적되므로 여기 `session/2026-08-13-06-commit-audit-dispatch-redesign-bugs.md`]
`base/effect-plan.md`가 쓰는 용어 `cleanup`은 잔여 stale이 아니라 의도된
별개 개념** — 매 감사마다 재지적되므로 여기
못박아 둠. 두 층위가 다름: 못박아 둠. 두 층위가 다름:
- **`retract`**: Handler 계약의 것. `process`가 반환하는 클로저로, quad - **`retract`**: Handler 계약의 것. `process`가 반환하는 클로저로, quad
**내부 배관**이 "이전 처리를 무른다". **내부 배관**이 "이전 처리를 무른다".

View file

@ -314,7 +314,7 @@ Modifier에는 없음).
하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용. 하나 + 자주 쓰는 것만 정적 필드로 미리 바인딩" 패턴 재사용.
(주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은 (주의: 이벤트는 이 관습의 유일한 예외라 인용 대상에서 제외 — 이벤트 바인딩은
PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/event-plan.md` PA님 방식인 문자열 키 + 런타임 리플렉션으로 감, `base/event-plan.md`
"이벤트 바인딩 정정" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스 "이벤트 바인딩 — self 미전달, false로 disconnect" 절 참고. Modifier는 이벤트가 아니라 Store/인스턴스
생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.) 생성과 같은 카테고리라 dot-access 관습이 그대로 적용됨.)
`mod:UICorner(8)`가 실제로 어떻게 UICorner 자식을 만들어 붙이는지(v1의 `mod:UICorner(8)`가 실제로 어떻게 UICorner 자식을 만들어 붙이는지(v1의

View file

@ -27,7 +27,7 @@
함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시 함.** 콜백 파라미터 타입은 호출부가 인라인으로 직접 명시
(`function(v: UDim2) ... end`) — Luau가 그 타입이 실제 프로퍼티 타입과 (`function(v: UDim2) ... end`) — Luau가 그 타입이 실제 프로퍼티 타입과
일치하는지 검증해주지 않음. 이미 확정된 "이벤트 바인딩은 콜백 시그니처를 일치하는지 검증해주지 않음. 이미 확정된 "이벤트 바인딩은 콜백 시그니처를
Luau가 검증 못 하는 대가를 받아들인다"는 결정(`event-plan.md` "이벤트 Luau가 검증 못 하는 대가를 받아들인다"는 결정(`base/bind-system-plan.md` "이벤트
바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도 바인딩 — `On.EventName` 도트액세스 안 씀" 절, "타입 안전성을 어느 정도
포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려 포기하는 대가")과 같은 급의 트레이드오프라 새로 정당화할 것 없음 — 오히려
`AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더 `AttributeKey<<T>>`처럼 제네릭으로 정확히 맞추려는 시도는 이벤트 키보다 더

View file

@ -206,8 +206,8 @@ dispatch-core-plan.md`의 "Length/Offset — 여러 Slot이 형제로 섞일 때
보장" 절 참고. **DOM류 물리 순서 백엔드에도 같은 base 메커니즘이 그대로 보장" 절 참고. **DOM류 물리 순서 백엔드에도 같은 base 메커니즘이 그대로
재사용됨**(offset이 바뀌어도 이미 마운트된 원소를 물리적으로 옮길 필요 재사용됨**(offset이 바뀌어도 이미 마운트된 원소를 물리적으로 옮길 필요
없음 — `insertBefore`가 뒤 형제를 자연히 밀어주므로, backend Handler의 없음 — `insertBefore`가 뒤 형제를 자연히 밀어주므로, backend Handler의
"offset 변경 시 할 일"만 no-op으로 달라짐) — `architecture.md`의 "다른 "offset 변경 시 할 일"만 no-op으로 달라짐) — `architecture.md`의 "패키지 경계"가
렌더 백엔드에서도 재사용 가능해야 한다"는 전제와도 부딪히지 않음. 세운 "다른 렌더 백엔드에서도 재사용 가능해야 한다"는 전제와도 부딪히지 않음.
## Slot과 Store 바인드의 관계 (`retract` 순서) ## Slot과 Store 바인드의 관계 (`retract` 순서)
@ -1020,7 +1020,7 @@ end` 관용구로 `userdata` 안에 담긴 리소스(예: `Observer:Subscribe()`
나쁨 — 사용자가 "정리가 보장된다"고 오해하고 `Subscribe`류를 `userdata` 나쁨 — 사용자가 "정리가 보장된다"고 오해하고 `Subscribe`류를 `userdata`
넣었다가 Destroy 경로에서 조용히 새는 게 실제로 훨씬 위험한 결과. 넣었다가 Destroy 경로에서 조용히 새는 게 실제로 훨씬 위험한 결과.
`retract`가 Destroy 시엔 절대 안 불린다는 기존 원칙(`base/ `retract`가 Destroy 시엔 절대 안 불린다는 기존 원칙(`base/
lifecycle-pattern.md` "quad는 라이프사이클 중간에 있지 않다")과 정확히 lifecycle-pattern.md` "quad는 자신이 만든 Instance의 라이프사이클")과 정확히
같은 이유로, `:List`에 새 반쪽짜리 예외를 만들 이유가 없음. 같은 이유로, `:List`에 새 반쪽짜리 예외를 만들 이유가 없음.
**대신 명시적 제약으로 문서화**: **`userdata`에는 반환된 element(또는 **대신 명시적 제약으로 문서화**: **`userdata`에는 반환된 element(또는
@ -1247,7 +1247,7 @@ future 재실행이 no-op됨(위 "`state:Observer(fn)`" 절 원칙 재사용)
`Relate(inst)`(weak-keyed) 아래 있어서 `inst`가 죽으면 그 안에 강참조로 `Relate(inst)`(weak-keyed) 아래 있어서 `inst`가 죽으면 그 안에 강참조로
붙잡혀 있던 Observer/클로저(`mounted`/`userdata`/`keyIndex`를 포함해)가 붙잡혀 있던 Observer/클로저(`mounted`/`userdata`/`keyIndex`를 포함해)가
전부 같이 GC 대상이 되는 것으로 공짜로 해결 — 명시적으로 구독을 끊는 전부 같이 GC 대상이 되는 것으로 공짜로 해결 — 명시적으로 구독을 끊는
새 코드가 필요 없음, `base/lifecycle-pattern.md`의 "정리는 기본적으로 새 코드가 필요 없음, `base/lifecycle-pattern.md`의 "정리(`retract`)는 기본적으로
GC에 위임" 원칙 그대로. GC에 위임" 원칙 그대로.
**부수 관찰(설계 아님, 메모만)**: `bindLifetime``Relate(inst)` 기반이라, **부수 관찰(설계 아님, 메모만)**: `bindLifetime``Relate(inst)` 기반이라,
@ -1597,8 +1597,8 @@ Roblox뿐 아니라 web에도 그대로 필요.
`offset`/`sum`은 0-based 개수(카디널 수)고, `_elements`/`updateFn`의 `offset`/`sum`은 0-based 개수(카디널 수)고, `_elements`/`updateFn`의
`index`는 1-based Lua 배열 관례 — `index + offset` 공식이 이 둘을 `index`는 1-based Lua 배열 관례 — `index + offset` 공식이 이 둘을
의도적으로 섞는 것. 상세는 `base/dispatch-core-plan.md`의 "0-based 의도적으로 섞는 것. 상세는 `base/dispatch-core-plan.md`의 "`offset`/`sum`은
개수" 절 참고. 0-based 개수" 절 참고.
## 반응형 raw 요소 — `State<T>`/`Source<T>`도 Slot 요소로 허용 (2026-08-11 일곱 번째 세션) ## 반응형 raw 요소 — `State<T>`/`Source<T>`도 Slot 요소로 허용 (2026-08-11 일곱 번째 세션)
@ -1883,8 +1883,8 @@ quad-roblox는 `inst:Destroy()`로 구현. 웹 등 다른 백엔드는 자기
**[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에 **[정정, 사용자 지적] "해제 짝"이라는 새 API는 필요 없음** — 옛 owner에
대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` + 대해 그냥 **`Dispatch.setOffsetSource(ownerKey, position, None)` +
`Dispatch.setLength(ownerKey, position, 0)`을 다시 부르면 끝**. `Dispatch.setLength(ownerKey, position, 0)`을 다시 부르면 끝**.
이건 이미 확정된 관용구 그대로임(`base/dispatch-core-plan.md`의 "실제 이건 이미 확정된 관용구 그대로임(`base/dispatch-core-plan.md`의 "해제(그
마운트를 하지 않는 위치는 `None`을 등록 — `setLength`도 짝을 맞춰 `0`"). 자리가 더 이상 기여하지 않게 될 때)는 `setOffsetSource(...,None)`").
즉 **해제 = 0/`None`으로 재등록**이고 별도 unregister 함수가 없어도 됨 — 즉 **해제 = 0/`None`으로 재등록**이고 별도 unregister 함수가 없어도 됨 —
앞서 "이게 실제 작업량"이라고 적었던 판단은 과했음. 앞서 "이게 실제 작업량"이라고 적었던 판단은 과했음.

View file

@ -691,7 +691,7 @@ someSource:Compute(computeFn)
이름 자체를 팩토리 함수로" — `Source(default)`, `Ref(default)`, 이름 자체를 팩토리 함수로" — `Source(default)`, `Ref(default)`,
`Store({defaults})`.** Ref도 예외 없이 이 스타일을 따름 — Ref가 `Store({defaults})`.** Ref도 예외 없이 이 스타일을 따름 — Ref가
`Ref()`로 안 만들어질 특별한 이유는 없었고(이전 절에서 API 모양만 `Ref()`로 안 만들어질 특별한 이유는 없었고(이전 절에서 API 모양만
다루고 생성자를 명시 안 해서 생긴 공백), `architecture.md`의 "복사 다루고 생성자를 명시 안 해서 생긴 공백), `architecture.md`의 "복사(clone)
구현 지양, 팩토리 함수로 대체" 원칙과도 정확히 일치. `Store({defaults})` 구현 지양, 팩토리 함수로 대체" 원칙과도 정확히 일치. `Store({defaults})`
같은 스타일로 지원(`defaults`는 선택 — 안 주고 `Store()`만 호출해도 같은 스타일로 지원(`defaults`는 선택 — 안 주고 `Store()`만 호출해도
됨, 순수 편의용 초기값 템플릿). 됨, 순수 편의용 초기값 템플릿).
@ -840,7 +840,7 @@ State/Source도 `:With`/`:Compute`마다 새 노드가 나오는 같은 모양
아니라 메소드 `state:Observer(fn)`로 확정** — `state`가 항상 필요한 아니라 메소드 `state:Observer(fn)`로 확정** — `state`가 항상 필요한
필수 인자라 `:` 리시버 자리에 자연스럽게 들어가고(다른 형태면 인자 필수 인자라 `:` 리시버 자리에 자연스럽게 들어가고(다른 형태면 인자
두 개짜리 자유 함수가 되어 읽는 순서가 어색해짐), `architecture.md` 두 개짜리 자유 함수가 되어 읽는 순서가 어색해짐), `architecture.md`
"함수지향 디폴트, `:` 체이닝은 예외적으로만(체이닝이 정말 편한 경우만)" "함수지향 디폴트, `:` 체이닝은 예외적으로만"
원칙이 정확히 이 경우를 가리킴 — Store 값 변경 체이닝과 같은 예외 원칙이 정확히 이 경우를 가리킴 — Store 값 변경 체이닝과 같은 예외
카테고리. **더 근본적인 이유**: 위 "독립 존재 가능한 프리미티브 vs 카테고리. **더 근본적인 이유**: 위 "독립 존재 가능한 프리미티브 vs
원천에 종속된 파생 데이터" 원칙 참고 — Observer는 State처럼 원천 없이는 원천에 종속된 파생 데이터" 원칙 참고 — Observer는 State처럼 원천 없이는

View file

@ -109,7 +109,7 @@ Store는 "이름 붙은 Source 모음, 그 이상 아님"으로 더 단순해짐
전용 미타입 폴백으로 격하돼 있었으므로 이번 정정과 무관하게 그대로 유지. 전용 미타입 폴백으로 격하돼 있었으므로 이번 정정과 무관하게 그대로 유지.
`:` 체이닝 원칙도 `:Set()` 자체가 그 사례라 유지. `:` 체이닝 원칙도 `:Set()` 자체가 그 사례라 유지.
`base/architecture.md`의 "복사 구현 지양, 팩토리 함수로 대체" 원칙과 함께 `base/architecture.md`의 "복사(clone) 구현 지양, 팩토리 함수로 대체" 원칙과 함께
읽을 것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며 읽을 것 — v1의 문제는 metatable 체이닝으로 매번 새 테이블을 할당하며
"불변 빌더"를 흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가 "불변 빌더"를 흉내낸 것이었지, `:` 체이닝 문법 자체나 커링 문법 자체가
아니었음. 아니었음.

View file

@ -30,6 +30,93 @@ haiku, 일반 작업은 sonnet. 특히 소스코드를 많이 읽어야 하는
사용자 본인 추정에 따라 여기 정식 관례로 승격(선택지 (a) 채택). 이 누락이 사용자 본인 추정에 따라 여기 정식 관례로 승격(선택지 (a) 채택). 이 누락이
아래 "사용자 발언을 인용할 때" 관례가 생긴 계기이기도 함. 아래 "사용자 발언을 인용할 때" 관례가 생긴 계기이기도 함.
## 문서 표기 규약
`doc-check.py`는 정규식으로 정적·결정론적으로 판정한다 — 표기가 흔들리면
검사가 열거형으로 비대해지거나 아예 무력화된다. **"예상 가능한 범위"를
컨벤션으로 미리 좁혀두는 게 정규식을 늘리는 것보다 싸다**는 게 사용자
판단(2026-08-16, "형식이 많이 왔다 갔다 하면 문제가 커져", "가장 비용이
쌈"). 근거가 된 실측과 논의 원문은
`session/2026-08-16-03-doc-check-section-convention.md`.
컨벤션이 규정으로 값을 하는 범위는 **"도구가 잡은 걸 표준 마커로 잠재우는"
검사뿐**이다. "위험한 문장을 먼저 찾아내야 하는" 검사(시한부 주장 탐지 등)는
표현을 열거하는 수밖에 없다 — 정규식은 *날짜를 안 붙인 문장*을 찾을 수 없기
때문. 그 잔여가 `quad-doc-auditor`의 몫이고 앞으로도 그렇다.
- **⭐ [2026-08-16 신설] 절 인용 규약 — 절 제목은 의역하지 말고 원문에서
잘라 쓸 것.** `` `<파일>.md`의 "절 제목" `` 형태로 인용할 때(예시의
`<파일>`은 자리표시자 — 실제 파일명처럼 적으면 `doc-check.py`가 깨진
참조로 잡는다):
- **`#` 헤딩**: 제목의 **연속된 부분문자열**이면 됨(줄여 쓰기 허용, 말
바꾸기 금지).
- **`**볼드**` 절**: 이 코퍼스가 하위 절로 쓰는 형태. 절로 인정되는 건
**빈 줄 다음에 오는 볼드**와 **리스트 항목(`- `/`1. `/`> `) 머리의
볼드**뿐이다 — 문단이 줄바꿈되며 우연히 줄머리에 걸린 강조는 절이
아니다. 인용은 제목 **앞부분부터** 잘라 쓸 것. 볼드는 단순 강조와
구분이 안 되고 `#` 헤딩보다 압도적으로 흔해서(실측 수치는
`session/2026-08-16-03-doc-check-section-convention.md`), 중간 조각
인용까지 허용하면 무관한 강조에 우연히 걸려 **검사가 조용히 통과하는**
쪽으로 무력화된다.
- **큰따옴표가 든 제목은 앞부분만 인용**할 것 — 인용 문법 자체가
`"..."`라 안쪽 따옴표가 문법을 깬다.
- **⚠️ 태그로 닫히는 볼드는 절로 인식되지 않는다.**
`- **[해소됨, …]** 실제 제목 — 본문…`처럼 볼드가 상태 태그에서 끝나고
진짜 제목이 볼드 **밖** 평문으로 나오면(`archive/question-resolved.md`가
이 형태), 검사기는 "[해소됨, …]"만 절 제목으로 본다. 그런 항목을
가리킬 땐 큰따옴표 인용을 쓰지 말고 백틱으로 쓸 것. **새로 쓸 땐 태그와
제목을 한 볼드 스팬 안에 넣을 것** — `**[해소됨, …] 실제 제목**`. 절 참조가
ERROR로 승격됐으므로, 이걸 모르고 규약대로 큰따옴표 인용을 하면 실재하는
서술인데도 커밋이 막힌다.
- 선두 장식(⚠️/⭐), 백틱, **선두 상태·날짜 태그**(`[2026-08-16 신설]`,
`[해소됨, …]`)는 붙이든 빼든 무관(양쪽에서 똑같이 벗겨내고 비교). 태그는
제목의 일부가 아니다 — 시간이 지나면 내용이 바뀌는 메타 표시라 거기에
인용을 묶으면 태그가 갱신될 때마다 참조가 깨진다. **인용은 태그 뒤
실제 제목부터 잘라 쓰는 걸 권장**.
- 줄바꿈에 걸쳐 인용해도 무관(공백을 무시하고 비교). 다만 인용이 **160자를
넘으면 검사 자체가 안 걸리니** 길게 늘어뜨리지 말 것.
- **blockquote(`> `) 안에서는 인용을 줄바꿈에 걸치지 말 것** — 다음 줄의
`> ` 마커가 인용문 안으로 딸려 들어가 매칭이 깨진다. 실제로 이 세션에
`ROADMAP.md`에서 그 마커를 지우다 blockquote를 통째로 망가뜨릴 뻔했다.
- **절 제목을 문장에 끼울 땐 "…" 뒤에 `절`을 붙일 것**(`"패키지 경계" 절이
세운 원칙`처럼). 제목을 명사구 자리에 그대로 넣으면 원문에서 잘라 쓴
제목이 문장과 안 맞아 문법이 깨진다 — 이 세션에 실제로 여러 건 냈다.
- **용어를 인용할 땐 이 형태를 쓰지 말 것** — `` `<파일>.md`의 "cleanup" ``
처럼 쓰면 절 인용과 문법이 같아서 구분이 안 된다(실제로
`base/lifecycle-pattern.md``effect-plan.md`의 용어 `cleanup`을 이
형태로 인용해 오탐을 냈었고, 이 규약을 세우면서 같이 고쳤다). 용어는
따옴표 없이 쓰거나 문장을 바꿀 것.
- 제외 대상: `initreq/`를 가리키는 인용(읽기 전용 외부 원본이라 절 구조가
없음 — `raw-userinput.md`는 스스로 "정리가 없는 생각의 흐름"이라 밝힘),
그리고 `archive/`·`session-summary.md`가 하는 인용(히스토리 문서).
- **왜 지금 정하나**: 예전엔 "절 제목을 의역해 인용하는 관례"가 있어서 이
검사가 영원히 WARN에 묶여 있었다. 의역을 금지하면 ERROR로 승격돼 절
참조 rot이 커밋 게이트에 걸린다 — `bind-system-plan.md` 분할 때 20곳이
이 사각지대로 샜던 실패 모드다. **[2026-08-16] 불일치를 0으로 만들고
깨끗하게 도는 걸 확인한 뒤 ERROR로 승격 완료** — 이제 의역 인용은
커밋 게이트에서 막힌다.
- **⭐ [2026-08-16 신설] 세션은 산문 서수 말고 파일 ID로 지칭할 것.**
지금 코퍼스엔 한글 서수("열세 번째 세션") / `N차` / `N번째`가 다 섞여
있고(정확한 분포는 세는 범위에 따라 달라지므로 여기 숫자로 못 박지 않음),
**같은 세션을 다른 이름으로 부르는 사례가 한 파일 안에도 있다**
(`base/tween-plan.md`가 같은 2026-08-13 13차를 "13차 세션"과 "열세 번째
세션" 두 표기로). 이건 스타일 취향이 아니라 **grep을 감사 도구로 못 쓰게
만드는 문제**다 — "13차 세션에 뭘 정했나"를 찾으면 절반을 놓친다. 앞으로는
`` `session/YYYY-MM-DD-NN-slug.md` `` 형태의 파일 ID로 쓸 것. `doc-check.py`
이 참조를 이미 검증하므로(없는 세션을 인용하면 깨진 파일 참조로 잡힘) 새
코드도 필요 없다. **기존 표기는 일괄 치환하지 말 것** — 그 파일을 만질 때
같이 고친다.
- **[2026-08-16 검토 후 보류] 날짜 마커 라벨(`[YYYY-MM-DD 신설/정정/기준…]`)의
어휘는 닫지 않는다.** 날짜 리터럴 자체는 강제 장치 없이도 이미 100%
균일해서(실측은 세션 파일) 고칠 게 없고, 라벨 쪽은 **사용자
판단**대로 기계 검사 대상이 아니라 읽는 쪽 판단 재료다 — "에이전트가 순서
섞였을 때 최신의, 옳은 요소 선택에 도움이 되는 정보에 가깝지 이게 warn을
만들지는 못할듯". 어휘를 닫아도 검사가 하나도 안 생기므로 안 한다.
같이 묶여 있던 `DATED` 앵커 좁히기(±3줄 아무 날짜 → 같은 줄 마커)만은
유일하게 WARN을 만드는 항목이라 성격이 다르지만, 지금 그 위음성이 아프다는
증거가 없어 함께 보류 — 아프면 그때 한다("실제로 관측된 문제에만 구조를
쓴다" 원칙 그대로).
## 작업 방식 ## 작업 방식
- **소스코드를 많이 읽어야 하는 리서치는 Agent(Explore)로 위임** — 메인 - **소스코드를 많이 읽어야 하는 리서치는 Agent(Explore)로 위임** — 메인
@ -74,9 +161,11 @@ haiku, 일반 작업은 sonnet. 특히 소스코드를 많이 읽어야 하는
6. **인덱스 레이어 3개를 같이 갱신**: `.claude/README.md`(색인), 6. **인덱스 레이어 3개를 같이 갱신**: `.claude/README.md`(색인),
`question.md`(사용자가 답할 것만), 루트 `ROADMAP.md`/`HUMAN_TODO.md`. `question.md`(사용자가 답할 것만), 루트 `ROADMAP.md`/`HUMAN_TODO.md`.
- **⭐ [2026-08-16 실측] `@import`로 로드되는 파일(이 문서/`project-context.md`/ - **⭐ [2026-08-16 실측] `@import`로 로드되는 파일(이 문서/`project-context.md`/
`todos.md`)을 그 세션에 고쳐도 **그 세션의 컨텍스트엔 반영되지 않는다.** `todos.md`)을 그 세션에 고쳐도 그 세션의 컨텍스트엔 반영되지 않는다.**
세션 시작 시점 버전이 그대로 남는다 — 4라운드 감사가 받은 임포트 블록이 세션 시작 시점 버전이 그대로 남는다 —
세션 시작 커밋과 정확히 일치했고(그 사이 7커밋), 메인 세션 자신의 `session/2026-08-16-02-audit-tooling-verification-and-first-real-run.md`
세션의 4라운드 감사가 받은 임포트 블록이 세션 시작 커밋과 정확히
일치했고(그 사이 7커밋), 메인 세션 자신의
컨텍스트도 마찬가지였다. **그래서 이 문서를 고친 세션은 자기가 방금 쓴 컨텍스트도 마찬가지였다. **그래서 이 문서를 고친 세션은 자기가 방금 쓴
규칙을 컨텍스트에서 못 본다** — 고친 뒤에는 기억에 의존하지 말고 파일을 규칙을 컨텍스트에서 못 본다** — 고친 뒤에는 기억에 의존하지 말고 파일을
직접 `Read`해서 따를 것. 다음 세션부터는 정상적으로 새 버전이 로드된다. 직접 `Read`해서 따를 것. 다음 세션부터는 정상적으로 새 버전이 로드된다.
@ -85,7 +174,10 @@ haiku, 일반 작업은 sonnet. 특히 소스코드를 많이 읽어야 하는
훑음. **커밋 전에 돌리는 게 기본** — 수동 감사에서 나온 발견의 대부분이 훑음. **커밋 전에 돌리는 게 기본** — 수동 감사에서 나온 발견의 대부분이
이걸로 잡히는 종류였고, 실제로 여덟 번째 세션에 문서를 쪼개면서 잘못 이걸로 잡히는 종류였고, 실제로 여덟 번째 세션에 문서를 쪼개면서 잘못
옮긴 참조를 이 스크립트가 잡아냈음. ERROR는 고치고, WARN은 판단이 옮긴 참조를 이 스크립트가 잡아냈음. ERROR는 고치고, WARN은 판단이
필요한 것(절 제목을 의역해 인용한 관례 등)이라 늘 0일 필요는 없음. 필요한 것이라 늘 0일 필요는 없음. **[2026-08-16 정정]** 예전엔 여기
"절 제목을 의역해 인용한 관례"를 WARN이 0일 수 없는 대표 사유로 적어뒀는데,
위 "절 인용 규약"으로 **의역 인용은 관례가 아니라 고칠 대상이 됐다**
절 참조는 이제 WARN이 아니라 **ERROR**다(승격 완료).
- **[2026-08-16 도입] `.claude/agents/quad-doc-auditor.md` 서브에이전트 — - **[2026-08-16 도입] `.claude/agents/quad-doc-auditor.md` 서브에이전트 —
위 체크리스트 2~4번(본문 문장 grep, archive 이전, 개수/목록 단일화)을 위 체크리스트 2~4번(본문 문장 grep, archive 이전, 개수/목록 단일화)을
신선한 맥락에서 대신 수행.** 읽기 전용, 발견만 리포트(직접 수정 안 함). 신선한 맥락에서 대신 수행.** 읽기 전용, 발견만 리포트(직접 수정 안 함).

View file

@ -77,7 +77,7 @@ ROADMAP 항목 근거인지, 어떻게 실행하는지, 실행 후 뭘 확인해
| `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source<T>``State<T>`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `base/source-state-plan.md` "Source가 State를 만족함", ROADMAP M0-2 | | `08-type-source-satisfies-state.luau` (타입체크 전용) | `Source<T>``State<T>`를 구조적으로 만족하는 제네릭 타입이 솔버에서 안전한지 | `base/source-state-plan.md` "Source가 State를 만족함", ROADMAP M0-2 |
| `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 | | `09-type-modifier-overridden-subtype.luau` (타입체크 전용) | `FrameModifier <: GuiObjectModifier`처럼 서브타입 관계인 Modifier를 `Overridden`으로 섞을 때 타입이 통과하는지 | `modifier-plan.md` 9-2번, ROADMAP M7 |
| `10-roblox-studio-checks.server.luau` (Studio 전용) | **[⚠️ 2026-08-14 다섯 번째 세션: A 섹션이 폐기된 모델을 검증 중 → `rewrite-required/`, 열한 번째 세션에 `canBound` 재도입으로 재작성 사유 하나 더 추가]** (A) `bindLifetime`/`unbindLifetime`/`canBound`/`canExecute`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복. **A는 재작성 대상** — 파일 속 옛 `canBound`(9차 세션 정의)와 `bindLifetime``value.Subscribed = true` 세팅, 2-인자 `canExecute(inst, value)`는 전부 낡음(현재 게이트는 이중 바인딩 확인은 `canBound(v)`, emit 게이팅은 `canExecute(v)` — 둘 다 `value` 단독 1-인자로 비공개 헬퍼를 공유, gcconn/gchold는 **Instance 생성 시점**에 생성). **[2026-08-13]** A 섹션 앞부분(ClassName 신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됐고 **새 모델에서도 그대로 유효**(오히려 더 중요 — `canBound`/`canExecute`가 `.Connected`를 직접 읽는 게 leaf 경로 판정의 전부), `audit/gcconn-trick-verification.md` 참고. 이중 바인딩 게이트/재바인딩 허용/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md` "`bindLifetime`/`canBound`/`canExecute`/`unbindLifetime` — 확정", `archive/canexecute-inst-arg-reversed.md`, `source-state-plan.md` "이중 바인딩 금지", `.claude/session-summary.md` 2026-08-06 세션, `debug-tooling-plan.md` | | `10-roblox-studio-checks.server.luau` (Studio 전용) | **[⚠️ 2026-08-14 다섯 번째 세션: A 섹션이 폐기된 모델을 검증 중 → `rewrite-required/`, 열한 번째 세션에 `canBound` 재도입으로 재작성 사유 하나 더 추가]** (A) `bindLifetime`/`unbindLifetime`/`canBound`/`canExecute`의 gcconn 트릭 + 이중 바인딩 게이트(Destroy 시 Connected 전환 포함), (B) Attribute의 Instance 참조 타입 지원, (C) CollectionService 태그/GetTagged 왕복. **A는 재작성 대상** — 파일 속 옛 `canBound`(9차 세션 정의)와 `bindLifetime``value.Subscribed = true` 세팅, 2-인자 `canExecute(inst, value)`는 전부 낡음(현재 게이트는 이중 바인딩 확인은 `canBound(v)`, emit 게이팅은 `canExecute(v)` — 둘 다 `value` 단독 1-인자로 비공개 헬퍼를 공유, gcconn/gchold는 **Instance 생성 시점**에 생성). **[2026-08-13]** A 섹션 앞부분(ClassName 신호 미발화, Destroy 시 Connected 즉시 전환)은 사용자 자작 스크립트로 부분 확인됐고 **새 모델에서도 그대로 유효**(오히려 더 중요 — `canBound`/`canExecute`가 `.Connected`를 직접 읽는 게 leaf 경로 판정의 전부), `audit/gcconn-trick-verification.md` 참고. 이중 바인딩 게이트/재바인딩 허용/B/C는 이 공식 파일로 아직 확인 안 됨 | `lifecycle-pattern.md` "`bindLifetime`/`canBound`/`canExecute`/`unbindLifetime` — 확정", `archive/canexecute-inst-arg-reversed.md`, `source-state-plan.md` "이중 바인딩 금지", `.claude/session-summary.md` 2026-08-06 세션, `debug-tooling-plan.md` |
| `11-modifier-illegal-value-error.luau` | Modifier 필드에 Ref/PreRef/Observer/Effect/Slot/Modifier가 들어오면 즉시 error, State/Source가 확정하는 값이 Modifier면 즉시 error(2026-08-09 세션에 "UB"에서 전환된 규칙) | `modifier-plan.md` "핸들러 계층 값 즉시 error" 절 + 7번 절 | | `11-modifier-illegal-value-error.luau` | Modifier 필드에 Ref/PreRef/Observer/Effect/Slot/Modifier가 들어오면 즉시 error, State/Source가 확정하는 값이 Modifier면 즉시 error(2026-08-09 세션에 "UB"에서 전환된 규칙) | `modifier-plan.md` "Modifier 필드에 핸들러 계층 값(Ref/PreRef/PostRef/Observer/Effect/Slot/Modifier)이 들어오면 즉시 error" 절 + 7번 절 |
| `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<<T>> "name"] = value`(구 `Attribute<<T>>`)처럼 제네릭 DI 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) | | `12-type-attribute-generic-key-narrowing.luau` (타입체크 전용) | `[AttributeKey<<T>> "name"] = value`(구 `Attribute<<T>>`)처럼 제네릭 DI 키를 쓸 때 `value`의 타입이 실제로 `T`로 좁혀지는지 — base 문서 자신이 "미검증"이라 명시한 항목 | `attribute-plan.md` "[실측 필요, M0/M10]" (2026-08-09 열한 번째 세션 신설) |
| `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>``Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유. **[2026-08-14 아홉 번째 세션] 재작성 시 `PostRef`도 같이 커버할 것** — 같은 `Ref` 런타임 재사용 + 브랜드 태그만 다른 형제라 A/B 둘 다 그대로 확장되고, Leaf predicate도 `isRef(v) and not isPreRef(v) and not isPostRef(v)`로 늘어남 | `brand-plan.md``Brand` 절(2026-08-09 열한 번째 세션 재정정) | | `13-type-ref-preref-subtype.luau` | (A, 타입) `PreRef<T>``Ref<T>`를 구조적으로 만족하는지, (B, 런타임) `isRef`/`isPreRef` 합성이 재정정대로 동작하는지(`isRef(preRefInstance)`가 이제 `true`) + Leaf 핸들러가 `isRef(v) and not isPreRef(v)`로 명시적으로 좁혀야 하는 이유. **[2026-08-14 아홉 번째 세션] 재작성 시 `PostRef`도 같이 커버할 것** — 같은 `Ref` 런타임 재사용 + 브랜드 태그만 다른 형제라 A/B 둘 다 그대로 확장되고, Leaf predicate도 `isRef(v) and not isPreRef(v) and not isPostRef(v)`로 늘어남 | `brand-plan.md``Brand` 절(2026-08-09 열한 번째 세션 재정정) |
| `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `source-state-plan.md` "State는 쓰기 대상이 아님" 절의 `default` 생략 캐비엇 | | `14-type-nilable-default-overload.luau` (타입체크 전용) | `Source(default)`/`Ref(default)`의 `default` 생략이 `T`가 nilable일 때만 안전하다는 캐비엇을, 함수 오버로드(교차 타입)로 실제로 타입 레벨에서 막을 수 있는지 | `source-state-plan.md` "State는 쓰기 대상이 아님" 절의 `default` 생략 캐비엇 |

View file

@ -56,7 +56,7 @@ React `key` prop, Vue `v-for :key`, Solid `<For>`, Fusion `ForPairs`/
**최종 확정 형태는 자유 함수도 새 타입도 아니라 `Slot`의 콜론 메소드** **최종 확정 형태는 자유 함수도 새 타입도 아니라 `Slot`의 콜론 메소드**
(`Slot():List(data, updateFn, keyFn?) -> Slot`) — 상세 시그니처/구현 (`Slot():List(data, updateFn, keyFn?) -> Slot`) — 상세 시그니처/구현
의사코드/왜 자유 함수·새 타입이 아닌지/`Move` 기반 리오더/`userdata` 기반 의사코드/왜 자유 함수·새 타입이 아닌지/`Move` 기반 리오더/`userdata` 기반
`Source` 관리 위임은 전부 `base/slot-plan.md`의 "`Slot:List(...)`" 절 `Source` 관리 위임은 전부 `base/slot-plan.md`의 "`Slot:List(data, updateFn, keyFn?)`" 절
참고, 여기서 반복 안 함. 참고, 여기서 반복 안 함.
이 아래 있던 "왜 매핑 함수 직관이 안 통하는가"/"메커니즘 스케치"/ 이 아래 있던 "왜 매핑 함수 직관이 안 통하는가"/"메커니즘 스케치"/

View file

@ -322,7 +322,7 @@ UI를 클릭한 순간)에 강제로 그 계산을 트리거하는 건 이 전
실제로 알고 있는 가장 가까운 **부모**를 대신 선택/하이라이트. 실제로 알고 있는 가장 가까운 **부모**를 대신 선택/하이라이트.
- 플러그인 트리에 대응 노드가 **있는** 경우(예: 사용자가 어떤 Instance를 - 플러그인 트리에 대응 노드가 **있는** 경우(예: 사용자가 어떤 Instance를
직접 컴포넌트화해서 quad로 bind한 경우 — 이건 UB가 아니라 충분히 유효한 직접 컴포넌트화해서 quad로 bind한 경우 — 이건 UB가 아니라 충분히 유효한
사용법, `base/component-composition-plan.md`의 "컴포넌트 = 플레인 함수" 사용법, `base/component-composition-plan.md`의 "컴포넌트 = 그냥 함수"
원칙과도 맞음) 트리에 있는 그 실제 노드를 정확히 선택. 원칙과도 맞음) 트리에 있는 그 실제 노드를 정확히 선택.
**네이밍 컨벤션(사용자 제안)**: 내부 자동 생성 helper Instance는 `_` **네이밍 컨벤션(사용자 제안)**: 내부 자동 생성 helper Instance는 `_`
@ -330,7 +330,7 @@ UI를 클릭한 순간)에 강제로 그 계산을 트리거하는 건 이 전
이름 바꾸는 건 비용이 크지 않음. v1이 이미 `_quad_round`/`_quad_padding`/ 이름 바꾸는 건 비용이 크지 않음. v1이 이미 `_quad_round`/`_quad_padding`/
`_quad_scale` 네이밍(`base/ui-shorthand-plan.md` 참고)으로 정확히 `_quad_scale` 네이밍(`base/ui-shorthand-plan.md` 참고)으로 정확히
이 관습을 썼던 전례 — quad-v2에서 내부 자동 생성물이 생기면 그대로 이 관습을 썼던 전례 — quad-v2에서 내부 자동 생성물이 생기면 그대로
재사용. `research/documentation-plan.md`의 "UI 네이밍 컨벤션 문서" 재사용. `research/documentation-plan.md`의 "UI 요소 네이밍 컨벤션 문서"
백로그에도 이 구체적 규칙을 추가해둠. 백로그에도 이 구체적 규칙을 추가해둠.
**플러그인 UI 아키텍처 확인(사용자 질문에 대한 답 — 맞음)**: 세 개의 **플러그인 UI 아키텍처 확인(사용자 질문에 대한 답 — 맞음)**: 세 개의

View file

@ -61,7 +61,7 @@ ERROR, `doc-check.py` 파이프라인에 편입) 두 모드.
- **[2026-08-16 기준] 지금 구조**: `.claude/session/YYYY-MM-DD-NN-slug.md` - **[2026-08-16 기준] 지금 구조**: `.claude/session/YYYY-MM-DD-NN-slug.md`
세션 원문 전체가 있고, `.claude/session-summary.md`(CLAUDE.md 분할 전엔 세션 원문 전체가 있고, `.claude/session-summary.md`(CLAUDE.md 분할 전엔
`CLAUDE.md`의 "세션 히스토리" 절이었음)에 사람이 손으로 압축한 2~4줄 분할 전 `CLAUDE.md`에서 `세션 히스토리` 절이었음)에 사람이 손으로 압축한 2~4줄
요약 + 링크가 **별도 텍스트로** 적혀있음 — 한쪽만 갱신되면 어긋날 수 요약 + 링크가 **별도 텍스트로** 적혀있음 — 한쪽만 갱신되면 어긋날 수
있는 구조. 있는 구조.
- **적용 후**: 각 세션 파일 안에 "이게 이 세션의 정본 요약"이라고 표시하는 - **적용 후**: 각 세션 파일 안에 "이게 이 세션의 정본 요약"이라고 표시하는

View file

@ -142,6 +142,7 @@ React 커스텀 훅만큼의 합성성을 실사용 규모에서 주는가"도
이전 완료, 상세 근거는 3번 절 참고. 2번은 이제 전부 해소된 항목만 남음, 이 이전 완료, 상세 근거는 3번 절 참고. 2번은 이제 전부 해소된 항목만 남음, 이
문서 자체는 더 이상 사용자 판단 대기 상태가 아님. 문서 자체는 더 이상 사용자 판단 대기 상태가 아님.
- 1번 강점 목록은 `research/documentation-content-map.md`의 "왜 quad를 - 1번 강점 목록은 `research/documentation-content-map.md`의 "초심자(getting-started)
쓰는가" 초심자/quadnomicon 콘텐츠 소재로 재사용 가능(유일하게 남은 재활용 core loop"와 "`quadnomicon`" 축에 콘텐츠 소재로 재사용 가능(유일하게 남은
대상). 재활용 대상). 위 1번 헤딩이 말하는 "왜 quad인가" 문서는 아직 그 콘텐츠
맵에 항목으로 없음 — 쓰게 되면 거기 먼저 추가할 것.

View file

@ -104,8 +104,8 @@ price:Apply(addTaxAndShipping)
이미 `state:Apply(makeFormatter("ko-KR"))`를 "커링 팩토리 + `:Apply`"의 이미 `state:Apply(makeFormatter("ko-KR"))`를 "커링 팩토리 + `:Apply`"의
정석 예시로 들어둠 — `Operator.*`/`Animate`가 이 관용구를 따르는 게 정석 예시로 들어둠 — `Operator.*`/`Animate`가 이 관용구를 따르는 게
자연스러움. `Animate``:Compute`를 골랐던 건 오히려 이 기존 자연스러움. `Animate``:Compute`를 골랐던 건 오히려 이 기존
관용구에서 벗어난 예외였다는 게 이번 논의에서 드러남(`research/ 관용구에서 벗어난 예외였다는 게 이번 논의에서 드러남(`base/
tween-plan.md` "왜 `:Apply`인가로 정정" 절 참고). tween-plan.md` "왜 `:Apply`로 정정됐는가" 절 참고).
3. **일관성 — 0항/N항을 나누지 않음.** `Not`은 deps가 없어서 위 1번 3. **일관성 — 0항/N항을 나누지 않음.** `Not`은 deps가 없어서 위 1번
문제와 무관하지만, "이 라이브러리의 콤비네이터는 항상 `:Apply` 문제와 무관하지만, "이 라이브러리의 콤비네이터는 항상 `:Apply`
붙인다"는 단일 규칙을 지키는 게 "0항만 예외적으로 `:Compute` 붙인다"는 단일 규칙을 지키는 게 "0항만 예외적으로 `:Compute`

View file

@ -566,8 +566,7 @@ Modifier를 합친다"는 시나리오가 `Overridden`의 가장 그럴듯한
`base/tween-plan.md`(승격됨) "자연 완료(Completed) 시 per-instance `base/tween-plan.md`(승격됨) "자연 완료(Completed) 시 per-instance
북키핑 — 정리 안 해도 됨" 절. 아래는 원래 발견 당시 기록. 북키핑 — 정리 안 해도 됨" 절. 아래는 원래 발견 당시 기록.
**위치**: `base/tween-plan.md` "`retract`(구 cleanup)로 확정된 **위치**: `base/tween-plan.md` "override 정책" 절.
오버라이드 시맨틱" 절.
**문제**: "새 값이 들어와 갈아치울 때"의 `retract` 동작(4가지 옵션, 기본값 **문제**: "새 값이 들어와 갈아치울 때"의 `retract` 동작(4가지 옵션, 기본값
Cancel)은 상세히 정의했지만, **Tween이 사용자 개입 없이 스스로 끝까지 Cancel)은 상세히 정의했지만, **Tween이 사용자 개입 없이 스스로 끝까지
@ -662,9 +661,13 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
## 문서 모순 — 발견 현황 ## 문서 모순 — 발견 현황
### 이미 고침 (이번 세션) ### 이미 고침
- `architecture.md`의 "Store/State/Source 온톨로지 확정 요약" 절이 아래 첫 두 건은 이 감사 세션 자신이 정정했고, 세 번째는 나중 세션이
해소한 뒤 `session/2026-08-16-03-doc-check-section-convention.md`에서
"아직 안 고침"에 잘못 남아 있던 걸 발견해 여기로 옮긴 것이다.
- `architecture.md`의 "Store/State/Source 온톨로지 — 확정됨 (요약)" 절이
`source-state-plan.md`의 최신 재구성(Source가 State를 만족, `store.key` `source-state-plan.md`의 최신 재구성(Source가 State를 만족, `store.key`
Source를 직접 반환, `store.key:Set()`)을 못 따라가고 있던 것 — 이 감사 Source를 직접 반환, `store.key:Set()`)을 못 따라가고 있던 것 — 이 감사
세션 도중 발견해 직접 정정(커밋 `4b839b0`에서 별도로 이미 반영됨을 뒤늦게 세션 도중 발견해 직접 정정(커밋 `4b839b0`에서 별도로 이미 반영됨을 뒤늦게
@ -673,12 +676,10 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
`__newindex` 모델, 옛 "Ref=CreatedRef 자체" 정의를 그대로 담고 있던 것 — `__newindex` 모델, 옛 "Ref=CreatedRef 자체" 정의를 그대로 담고 있던 것 —
이번 세션에서 직접 정정. 이번 세션에서 직접 정정.
### 아직 안 고침 (판단 필요해서 여기 남김)
- **`State<Modifier>` 타입 차단(엔지니어링 비용 감수) vs Ref/Slot이 - **`State<Modifier>` 타입 차단(엔지니어링 비용 감수) vs Ref/Slot이
Modifier 필드에 들어가는 건 UB로 방치 — 같은 문서 안에서 정반대 원칙이 Modifier 필드에 들어가는 건 UB로 방치 — 같은 문서 안에서 정반대 원칙이
나란히 적용됨.** `base/modifier-plan.md` "Modifier는 핸들러 계층을 나란히 적용됨.** `base/modifier-plan.md`의 옛 `Modifier는 핸들러 계층을
모름" 절은 "권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는 모름` 절은 "권장 사용법은 아니지만 막을 이유도 없음 — 방어 로직 없는
UB로 남겨둠"이라 명시적으로 방어를 포기했는데, 바로 옆 7번 절은 UB로 남겨둠"이라 명시적으로 방어를 포기했는데, 바로 옆 7번 절은
`State<Modifier>` 조합을 "UB로 확정, **가능하면 타입 시스템으로 아예 못 `State<Modifier>` 조합을 "UB로 확정, **가능하면 타입 시스템으로 아예 못
넣게 막을 것**"이라며 정반대로 엔지니어링 비용을 들여 방어하기로 했다. 넣게 막을 것**"이라며 정반대로 엔지니어링 비용을 들여 방어하기로 했다.
@ -697,6 +698,9 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
아님 — 남은 차이는 `State<Modifier>` 쪽에 "되면 좋은 보너스"로 아님 — 남은 차이는 `State<Modifier>` 쪽에 "되면 좋은 보너스"로
타입 차단을 추가 시도해볼 여지가 있다는 것뿐(위 2-2번, 미검증이지만 타입 차단을 추가 시도해볼 여지가 있다는 것뿐(위 2-2번, 미검증이지만
더 이상 필수 방어선이 아니라 우선순위 낮음). 더 이상 필수 방어선이 아니라 우선순위 낮음).
### 아직 안 고침 (판단 필요해서 여기 남김)
- **Destroying 훅 신뢰도에 대한 서술이 `lifecycle-pattern.md` 내부에서도, - **Destroying 훅 신뢰도에 대한 서술이 `lifecycle-pattern.md` 내부에서도,
`framework-comparison-findings.md`와의 사이에서도 어긋남** — 위 1-6 `framework-comparison-findings.md`와의 사이에서도 어긋남** — 위 1-6
항목에 상세, 여기서는 "아직 아무도 하나의 확정 문장으로 정리 안 함"이라는 항목에 상세, 여기서는 "아직 아무도 하나의 확정 문장으로 정리 안 함"이라는
@ -742,6 +746,7 @@ Handler"라고만 서술해, 사실상 3개의 거의 동일한 형태(리터럴
**[2026-08-13 첫 실측 라운드 결과]** `17`**통과**(제네릭 `__index` + **[2026-08-13 첫 실측 라운드 결과]** `17`**통과**(제네릭 `__index` +
`table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염) — 다만 `table.clone` 체이닝, 메타테이블 참조 공유, 형제 분기 무오염) — 다만
최초 실행은 크래시했고 그 원인이 실은 **문서 결함**이었음(`modifier-plan.md`의 최초 실행은 크래시했고 그 원인이 실은 **문서 결함**이었음(`modifier-plan.md`의
`내부 구현` 절 본문에 있던
"데이터를 테이블에 직접 두고"가 self 최상위 리터럴 키로 읽히면 `__index` "데이터를 테이블에 직접 두고"가 self 최상위 리터럴 키로 읽히면 `__index`
`rawget` 성공 시 안 불려 두 번째 호출에서 죽음) — 문서 수정 후 통과. `rawget` 성공 시 안 불려 두 번째 호출에서 죽음) — 문서 수정 후 통과.
`16`**실패**`types.newfunction` 시그니처가 설치된 Luau 버전의 실제 `16`**실패**`types.newfunction` 시그니처가 설치된 Luau 버전의 실제

View file

@ -20,8 +20,8 @@ compat.lua나 어댑터 코드는 전혀 없고, README/커밋 메시지에도 "
`archive/question-resolved.md`가 "OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW는 `archive/question-resolved.md`가 "OOP 상속/커스텀 파서/Slot 스텁/`Pipe` COW는
확인된 죽은 접근"이라고 명시한 목록에 compat은 **포함돼 있지 않음**(정확한 서술). 확인된 죽은 접근"이라고 명시한 목록에 compat은 **포함돼 있지 않음**(정확한 서술).
`.claude/project-context.md`의 "반복 조사 금지"는 compat에는 적용되지 않는다 — 이 문서를 `base/bind-system-plan.md`의 "확정된 것" 절이 정한 반복 조사 금지 규칙은
쓰는 게 규칙 위반이 아님. compat에는 적용되지 않는다 — 이 문서를 쓰는 게 규칙 위반이 아님.
## 2. v1 공개 API 표면 — 두 계층으로 나뉨 ## 2. v1 공개 API 표면 — 두 계층으로 나뉨
@ -61,7 +61,7 @@ v1(`.claude/initreq/quad/src`) 조사 결과, API는 성격이 다른 두 계층
### 3-1. (a)는 얇게 재현 가능 — opt-in 서브패키지로 격리하면 근거 문제도 해소됨 ### 3-1. (a)는 얇게 재현 가능 — opt-in 서브패키지로 격리하면 근거 문제도 해소됨
- **이벤트 self 관습**: 클로저 한 겹으로 재현 가능. `base/event-plan.md` - **이벤트 self 관습**: 클로저 한 겹으로 재현 가능. `base/event-plan.md`
"이벤트 핸들러는 self를 받지 않는다" 절이 든 반대 근거 4가지(Ref 중복 "이벤트 핸들러는 self(Instance)를 받지 않는다" 절이 든 반대 근거 4가지(Ref 중복
채널, Modifier 정적 flatten과 경쟁, quad-debug 추적 밖 mutate 경로, 클로저 채널, Modifier 정적 flatten과 경쟁, quad-debug 추적 밖 mutate 경로, 클로저
비용)는 **코어에 넣을 때** 문제가 되는 것들 — 별도 opt-in 패키지 비용)는 **코어에 넣을 때** 문제가 되는 것들 — 별도 opt-in 패키지
(`quad-compat` 부활)로 격리하면 비용은 compat 사용자만 부담하고 코어 KV (`quad-compat` 부활)로 격리하면 비용은 compat 사용자만 부담하고 코어 KV
@ -250,7 +250,7 @@ v2 트리 안에 과거 v1 컴포넌트를 리프로 박아넣는 것, (B) 기
"직접 Destroy 금지, Unmount 경유" 규칙을 Dispatch 엔진의 어느 지점에 "직접 Destroy 금지, Unmount 경유" 규칙을 Dispatch 엔진의 어느 지점에
훅으로 강제할지도 Slot 실제 구현 시점 확인 필요. 훅으로 강제할지도 Slot 실제 구현 시점 확인 필요.
- **결론: 지금 결정 불가.** M0 이후 Slot 코어 로직 구현 라운드 - **결론: 지금 결정 불가.** M0 이후 Slot 코어 로직 구현 라운드
(`archive/question-resolved.md`의 "여러 Slot이 형제로 섞일 때 순서 보장" (`archive/question-resolved.md`의 `여러 Slot이 형제로 섞일 때 순서 보장`
항목과 같은 시점 — 이미 해소돼 아카이브로 옮겨졌으나 실제 구현 시점의 항목과 같은 시점 — 이미 해소돼 아카이브로 옮겨졌으나 실제 구현 시점의
참고 자료로는 여전히 유효)에서 이 두 가지를 실제 구현과 함께 재확인해야 함. 참고 자료로는 여전히 유효)에서 이 두 가지를 실제 구현과 함께 재확인해야 함.

View file

@ -1393,3 +1393,46 @@ blob과 바이트 단위로 동일, 메인이 `git rev-parse`로 독립 확인).
`const` 건은 사용자가 전제를 정정 — pesde의 types emit 같은 툴링 체인 지원 `const` 건은 사용자가 전제를 정정 — pesde의 types emit 같은 툴링 체인 지원
시점은 에이전트가 관측할 수 없으므로 `HUMAN_TODO.md` 8번(사용자가 파악하거나 시점은 에이전트가 관측할 수 없으므로 `HUMAN_TODO.md` 8번(사용자가 파악하거나
가능해질 때 알림)으로 이관. 가능해질 때 알림)으로 이관.
## 2026-08-16 세 번째 세션 — 표기 컨벤션으로 doc-check 정규식 줄이기
원문: [`session/2026-08-16-03-doc-check-section-convention.md`](session/2026-08-16-03-doc-check-section-convention.md)
사용자 제기 — `doc-check.py`가 정규식으로 결정론적 판정을 하는데 표기가
흔들리면 문제가 커지니, 정규식을 늘리기보다 "예상 가능 범위"를 컨벤션으로
좁히는 게 싸지 않냐. **실측 결과 날짜 표기는 이미 문제가 아니었다**
`20NN-NN-NN` 1864건 중 1864건이 균일(강제 장치 없이). 드리프트는 다른 데
있었고, WARN 86건 중 **절 참조 불일치가 78건(91%)**이었다.
**핵심 발견: 78건은 코퍼스가 지저분한 게 아니라 검사기가 못 읽는 것이었다.**
이 코퍼스는 `**볼드**` 선두 줄을 하위 절로 쓰는데 `headings()``#`
수집했다. 다만 볼드를 통째로 인정하면 검사가 장식이 된다(볼드는 `#` 헤딩보다
압도적으로 흔함 — 수치는 세션 원문). 여러 규칙을 실측 대조해 **`#` 헤딩은
부분문자열, 볼드 절은 빈 줄/리스트 머리 + 앞부분일치**라는 비대칭 규칙 채택 —
느슨한 규칙과 해소 건수가 같으면서 매칭 표면만 좁다. 부수로 인용 길이 상한
60→160자(넘으면 검사에서 **조용히** 빠져나갔음), 공백 무시 비교(줄바꿈 인용
대응), 선두 상태·날짜 태그 정규화, `initreq/` 대상 면제.
**결과: 절 참조 불일치 78 → 0.** 36건은 검사기 수정으로 사라졌고(애초에
위양성이었다는 뜻), 42건은 인용을 실제 절 제목으로 손으로 고쳤다 — 라운드별
내역은 세션 원문이 소스. 마지막 15건은 서브에이전트 3개에 병렬
위임해 추적했고 — **설계 서술이 유실된 건은 0건**, 대부분 "애초에 절이
아닌 것(코드 주석·본문 문장·의역)을 절로 인용해온 것"이었다. 부수로
`onchange-plan.md`가 9차 분할 때 일부러 안 옮긴 절을 잘못된 파일로 가리키던
것, `brand-plan.md`가 이미 이행된 정정을 "정정 대상"이라 부르던 것이 드러나
같이 고침.
**커밋 전 감사가 이 세션 자신의 실수를 하나 잡았다** —
`pre-implementation-audit.md`의 "아직 안 고침" 절에 있는
`State<Modifier>` 항목이 해소된 것 같아 해소 마커를 달았는데, 바로 아래
문단에 **2026-08-09 세션이 단 `[완전 해소]` 마커가 이미 있었다.** 중복인
데다 해소 시점을 2026-08-16으로 잘못 읽히게 만들어 되돌림. 이 항목이
해소 표시를 달고도 "아직 안 고침" 헤더 아래 남아 있는 것은 이 세션 이전부터
있던 별개 부채라 사용자에게 보고했고, **사용자 결정으로 "이미 고침" 절로
옮김** — 그 헤더 아래엔 이제 진짜 미해소인 `Destroying` 훅 건만 남는다.
사용자 결정 셋 — (1) 절 인용 규약 + 세션 파일 ID 지칭 채택, (2) 날짜 마커
라벨 어휘 닫기는 **기각**("에이전트가 순서 섞였을 때 최신의, 옳은 요소 선택에
도움이 되는 정보에 가깝지 이게 warn을 만들지는 못할듯" — 기계 검사 대상이
아니라 읽는 쪽 판단 재료), (3) (C) 추적은 컨텍스트 보호를 위해 서브에이전트
위임. `conventions.md`에 "문서 표기 규약" 절 신설.

View file

@ -0,0 +1,270 @@
# 2026-08-16 세 번째 세션 — 표기 컨벤션으로 doc-check 정규식 줄이기
전 세션(`session/2026-08-16-02-audit-tooling-verification-and-first-real-run.md`)
인수인계를 받고 시작. 설계는 안 건드림, 문서 툴링만.
## 사용자 문제 제기
> 현행 md 툴링 py 상 날짜 적는거 형식 같은것도, 강제화 하게 하는게 맞다 생각이
> 드는듯. doc-check.py 자체가 regex 를 여럿 사용해 정적으로, 결정론적이게
> 분석하는데, 형식이 많이 왔다 갔다 하면 문제가 커져. 여러 regex 를 추가해가며
> 하는것도 좋지만, '예상 가능 범위' 를 어느정도 컨벤션으로 정하는게 좋다
> 생각하는데(가장 비용이 쌈), 어떻게 봄?
## 실측 — 날짜는 이미 문제가 아니었다
전수 조사:
```
20NN-NN-NN 형태 1864건 / 1864건 (100%)
다른 구분자(. 또는 /) 0건
비-ASCII 공백 혼입 0건
대괄호 마커 383건
```
**강제 장치가 없는데 100% 균일**. 사용자 직감("싼 컨벤션이 통한다")을 반증하는
게 아니라 오히려 뒷받침하는 데이터라, 이걸 근거로 삼되 드리프트가 실제로 있는
곳으로 방향을 돌렸다.
`doc-check.py` 당시 출력: 라이브 문서 88개, ERROR 0, **WARN 86건**
그중 **절 참조 불일치가 78건(91%)**. 날짜 없는 주장은 3건뿐.
> **재검증 시 주의**: 이 78은 **실제 작업 트리** 기준이다. `git archive HEAD`
> 같은 걸로 격리 체크아웃해 돌리면 72가 나오는데, 그건 아카이브에
> gitignore된 `.claude/initreq/`가 안 들어가서 `raw-userinput.md`를 가리키는
> 인용들이 "절 참조 불일치"가 아니라 "깨진 파일 참조"로 분류되기 때문이다
> (커밋 전 4라운드 감사가 실제로 이 차이에 걸렸다). 숫자를 대조할 땐 트리를
> 갈아엎지 말고 `git show HEAD:<경로>`로 파일 단위 비교를 쓸 것.
## 프레이밍 교정 — 컨벤션은 정규식의 대안이 아니다
사용자가 "regex를 늘릴까 / 컨벤션을 정할까"의 양자택일로 물었지만, 실제
관계는 **컨벤션이 정규식을 열거형에서 앵커형으로 바꿔 작게 만드는 것**이다.
`DATED`가 "±3줄 안에 아무 날짜나 있으면 통과"라는 느슨한 판정을 하는 이유가
정확히 앵커할 표준 마커가 없어서다.
동시에 컨벤션이 못 고치는 영역도 명확히 했다 — 검사는 두 종류다.
- **(A) 이스케이프 해치형**: 도구가 위험 패턴을 잡고 사람이 표준 마커로
잠재움(`REF` 절 인용, `DATED`, `OURS`). 컨벤션이 곧바로 이득.
- **(B) 탐지형**: 도구가 위험한 문장을 먼저 찾아야 함(`TEMPORAL`의 6개
한국어 표현, `'아직 반영 안 됨'` 리터럴). **컨벤션이 원리적으로 못 고침**
"시한부 주장에 날짜를 붙여라"를 정해도 정규식은 *날짜를 안 붙인 문장*을
못 찾는다. 이 잔여가 `quad-doc-auditor`의 몫.
## 우선순위 제안과 사용자 결정
1. 절 인용 규약 + ERROR 승격 (WARN의 91%)
2. 세션 지칭 표기 (grep을 깨뜨림)
3. 날짜 마커 라벨 어휘 닫기
사용자 결정: **1+2 채택, 3 기각.** 3에 대한 논거가 날카로웠음 —
> 3. 의 경우 사실상 에이전트가 순서 섞였을 때 최신의, 옳은 요소 선택에 도움이
> 되는 정보에 가깝지 이게 warn 을 만들지는 못할듯.
즉 라벨은 **기계 검사 대상이 아니라 읽는 쪽(에이전트)의 판단 재료**다. 어휘를
닫아도 검사가 하나도 안 생기므로 비용만 든다. 3에 딸려 있던 `DATED` 앵커
좁히기만은 유일하게 WARN을 만드는 항목이라 성격이 다르지만, 지금 그 위음성이
아프다는 증거가 없어 같이 보류.
## 핵심 발견 — 78건은 대부분 "코퍼스가 지저분한 것"이 아니라 "검사기가 못 읽는 것"이었다
`dispatch-core-plan.md``#` 헤딩이 11개뿐인데 다른 문서들이 인용하는
"우선순위 동률/매치 실패 처리", "핸들러 내부 상태 저장", "props 순회 순서"는
전부 실재한다 — **`**볼드**`로 시작하는 줄**로. `headings()``#` 줄만
수집하니 이게 통째로 안 보였다.
그런데 볼드를 통째로 절 제목으로 인정하면 검사가 장식이 된다:
| 파일 | `#` 헤딩 | 볼드(전체) | 볼드(줄머리) |
|---|---|---|---|
| `dispatch-core-plan.md` | 11 | 264 | 130 |
| `slot-plan.md` | 40 | 331 | 162 |
| `architecture.md` | 9 | 60 | 23 |
세 규칙을 실측 대조:
```
A 느슨 볼드 + 부분문자열 해소 30
B 줄머리 볼드 + 부분문자열 해소 32
C 줄머리 볼드 + 앞부분일치 해소 26 → 선두 장식(⚠️) 정규화 추가 후 30
```
**C가 A와 해소 건수는 같으면서 매칭 표면만 좁다.** C 채택. `#` 헤딩은
부분문자열, 볼드 절은 앞부분일치라는 **비대칭**이 핵심 — 헤딩은 명시적 절
마커라 오인 위험이 없고, 볼드는 강조와 구분이 안 되니 더 엄격해야 한다.
부수로 `1. **…**`(번호 리스트 볼드)도 절로 인정하도록 넓힘 —
`conventions.md`의 핸드오버 체크리스트가 정확히 그 형태고 다른 문서가 그걸
절로 인용한다.
## `initreq/` 면제
`raw-userinput.md``.claude/initreq/`에 있는 읽기 전용 원본 요청이고, 스스로
"진짜 생각의 흐름 대로 작성되어있으며 정리가 없음"이라 밝힌 문서다. 여길
가리키는 인용은 절 제목이 아니라 원문 문장을 따오는 것이라 헤딩 대조가 애초에
무의미 → 폴더 범주로 면제. (`is_history()`를 넓히는 건 오답이었음 — 그건
*인용하는 쪽* 문서를 면제하는 스위치인데 여기선 *인용 대상*이 문제였다.)
## 결과
```
절 참조 WARN 78 → 42 (검사기 수정) → 23 (인용 19건 교정)
→ 15 (인용 8건 추가 교정) → 0 (추적 후 15건 교정)
그리고 WARN → ERROR 승격
```
교정한 인용은 대부분 "실제 절이 명확히 존재하는데 의역돼 있던 것". 대표적으로
`architecture.md``복사(clone) 구현 지양`을 "복사 구현 지양"으로,
`이벤트 핸들러는 self(Instance)를 받지 않는다`를 "self를 받지 않는다"로 줄여
쓴 것들. `operator-sugar-plan.md``tween-plan.md``research/`→`base/`로
승격된 걸 안 따라와서 경로도 같이 고침.
큰따옴표가 든 절 제목은 인용 문법(`"..."`)을 깨므로 앞부분만 인용하는 규칙이
여기서 나왔다(`pluggable 디스패치 엔진 자체도 "인터페이스"로 base가 소유`).
## 마지막 15건 — 서브에이전트 3개에 병렬 위임
"그 절이 어디로 갔는가"를 따져야 해서 읽을 양이 많았고, 메인 컨텍스트를
지키려고 대상 파일별로 3등분해 위임했다(읽기 전용, 발견만 리포트).
**총평: 설계 서술이 유실된 건은 0건.** 대부분 "애초에 절이 아닌 것을 절로
인용해온 것"이었다 — 코드 주석 한 줄(`클로저 생존이 곧 gchold 생존`),
본문 산문(`데이터를 테이블에 직접 두고`), 앞으로 쓸 문서의 주제명
(`왜 quad를 쓰는가`), 그냥 의역(`bind는 유일 슬롯`).
부수로 드러난 것 — `onchange-plan.md`가 9차 분할 때 **일부러 안 옮긴** 절을
`event-plan.md`로 잘못 가리키고 있었고(실제 위치는 `bind-system-plan.md`),
`brand-plan.md`는 2026-08-07에 이미 이행된 정정을 아직 "정정 대상"이라
부르고 있었다.
## 검사기 보완 — 조용한 통과 구멍 둘
에이전트들이 검사기 자체의 결함을 짚었다.
1. **인용이 60자를 넘으면 `REF`가 매칭 자체를 안 해 검사에서 조용히
빠져나갔다.** 위양성보다 나쁜 종류라 상한을 160자로 넓힘. 그러자 3건이
새로 드러났는데 전부 **줄바꿈 때문에 원문에 없는 공백이 인용에 끼어든**
케이스여서, 비교를 공백 무시로 바꿔 순증 작업량 0으로 닫음.
2. **선두 상태/날짜 태그**(`**[2026-08-16 신설]** 제목…`)가 볼드 절 제목의
앞부분을 차지해 어떤 제목형 인용도 매칭 불가였다. 태그는 시간이 지나면
바뀌는 메타 표시라 제목의 일부가 아니므로 양쪽에서 벗겨내고 비교.
이건 ERROR 승격 직후 **게이트가 내가 방금 쓴 인용 두 개를 잡아서** 발견됐다.
## 인용을 짧게 자르다 문장을 깬 것들 — 프로즈 패스에서 회수
정규식이 통과해도 의미는 깨질 수 있다. 커밋 전 diff를 "인용 매칭"이 아니라
"문장 읽기" 모드로 다시 훑어 회수한 것들:
- **`ROADMAP.md`의 blockquote를 통째로 깨뜨렸었다** — 인용이 `> ` 줄바꿈에
걸쳐 있어 마커가 인용문 안에 들어가 있었고, 그걸 지우면서 인용 블록이
무너졌다. 규약에 함정으로 명시.
- 절 제목을 명사구 자리에 그대로 끼워 **문법이 깨진 문장 5건**
(`"pluggable 디스패치 엔진 자체도"는 원칙에…`). `"…" 절이 세운 원칙`
형태로 교정하고 이것도 규약에 넣음.
- 축약하다 주장이 빠진 인용 1건(`README.md`의 "빈 자리 아님" 결론).
## 1라운드 감사 — 규칙 자체의 정밀도 구멍
두 번째 감사자가 더 중요한 걸 짚었다 — `attribute-plan.md`
`dispatch-core-plan.md`가 인용한 `architecture.md`
`**pluggable 디스패치 엔진 자체도 …**`**절이 아니다.** 한 문단이
줄바꿈되면서 강조 스팬이 우연히 줄머리에 걸린 자리이고, 진짜 절은 두 줄 위의
`**패키지 경계**`다. 같은 원문을 인용하는 `slot-plan.md:209`는 이미 올바르게
`"패키지 경계"`를 쓰고 있어서, **같은 diff 안에 옳은 인용과 우연한 인용이
공존**하고 있었다.
즉 "줄머리 볼드"라는 규칙만으로는 절과 랩된 강조를 못 가른다. 진짜 절 제목은
**빈 줄 다음에 오거나 리스트 항목 머리**라는 성질이 있어서 그걸 조건으로
추가했다. 실측 결과 **정확히 그 가짜 인용 2건만 깨지고 나머지 46건은 그대로
통과**했고, 볼드 후보 풀은 `dispatch-core-plan.md` 기준 131→104로 좁아졌다.
위양성 0으로 정밀도만 올린 셈.
교훈: 이 규칙은 "사람이 절이라고 인식하는 것"을 근사하는 휴리스틱이라,
근사가 어긋나는 자리는 실제 인용을 감사해서만 드러난다. 검사기가 통과시켰다고
그게 절인 건 아니다.
## 1라운드 감사 — 이 세션 자신의 실수
`pre-implementation-audit.md`의 "아직 안 고침" 절에 있는 `State<Modifier>`
비일관성 항목이 해소된 것으로 보여 `**[해소됨 — 2026-08-16 확인]**` 마커를
달았는데, **바로 다음 문단에 2026-08-09 세션이 단 `**[완전 해소, 2026-08-09
세션]**`이 이미 있었다.** 중복일 뿐 아니라 해소 시점을 일주일 뒤로 잘못
읽히게 만들어 되돌렸다. 서브에이전트(추적 담당)가 "후임 절이 옛 서술을
뒤집었다"는 근거를 정확히 줬지만 **그 항목이 이미 해소 표시를 달고 있다는
사실은 안 봤고, 메인 세션도 그 문단 아래를 안 읽었다** — 근거가 맞아도
"이미 처리돼 있지 않은가"를 따로 확인해야 한다는 교훈.
이 항목이 해소 표시를 달고도 "아직 안 고침" 헤더 아래 남아 있는 헤더-내용
불일치는 이 세션 이전부터 있던 별개 부채라 임의로 재배치하지 않고 사용자에게
물었고, **"이미 고침" 절로 옮기라는 결정**을 받아 반영했다. 그 헤더 아래엔
이제 진짜 미해소인 `Destroying` 훅 건만 남는다. 감사 세 라운드가 전부 이
자리를 지목했는데, 세 번 다 "고칠지 말지가 판단 문제"라 서브에이전트는
결론을 못 냈다 — 메인 세션만 사용자에게 물을 수 있다는 게 이 절차의 핵심인
이유다.
## 2라운드 감사 — "규칙을 세운 세션이 그 규칙을 어긴다"
1라운드 발견을 반영한 뒤 2라운드를 돌렸더니 또 실질 발견 2건이 나왔다.
둘 다 **이번 세션이 새로 생산한 산문**의 문제였다.
1. **`base/brand-plan.md`에 이번에 새로 쓴 문장이 "세션은 파일 ID로
지칭할 것" 규약을 어겼다** — 같은 커밋에서 만든 규약을 같은 커밋의 새
문장이 위반. `2026-08-07 여덟 번째 세션` → 세션 파일 ID로 교정했고,
이번에 건드린 다른 두 줄(`README.md`의 `tools/` 행, `lifecycle-pattern.md`)도
규약의 "만지는 김에 같이 고친다" 조항대로 처리했다.
2. **`research/framework-comparison-findings.md`의 수정이 "고침"이 아니라
"검사망 회피"였다** — `"왜 quad를 쓰는가"` 인용을 문법만 비껴가게 고쳐서
검사 대상에서 통째로 빠졌는데, 감사자가 `REF` 정규식으로 `group(3)`
`None`이 되는 걸 직접 확인해 잡았다. 같은 diff의 다른 두 회피
(`CLAUDE.md`의 "세션 히스토리", `옛 Modifier는 핸들러 계층을 모름`)는
**과거형으로 참인 사실**을 서술하니 정당하지만, 이건 대상 문서에 근거가
없는 **미래형 주장**이었다는 게 판정 근거. 실제 절(`초심자
(getting-started) core loop`/`quadnomicon`)을 가리키도록 고쳤다.
부수로, `conventions.md`에 박아둔 세션 서수 분포 숫자(971/68/8)가
재현되지 않았다 — 내 집계는 `session/`까지 포함한 전체 grep이었고 감사자는
`doc-check.py`가 훑는 88개 라이브 문서 기준이었다. **집계 범위를 안 적은
숫자를 매 세션 로드되는 파일에 박은 것 자체가 이번에 세운 "개수는 소스
하나만" 원칙 위반**이라 정성적 서술로 바꿨다. 같은 이유로 날짜 균일 수치도
`conventions.md`에서 빼고 이 파일을 가리키게 했다.
**교훈**: 규약을 세우는 커밋이 그 규약의 첫 위반자가 된다. 규약을 쓴
세션은 자기 diff를 그 규약으로 한 번 되짚어야 하고, 그걸 자기 self-audit
으로는 잘 못 한다(이번에도 둘 다 서브에이전트가 잡았다).
## 3라운드 감사 — 수정이 만든 새 결함, 그리고 사각지대 하나
2라운드 수정을 반영하고 3라운드를 돌렸더니 **내가 2라운드에 만든 마크다운
버그**가 나왔다. `"cleanup"` 용어 인용을 규약대로 고치려고 볼드 스팬을
쪼개다 **짝 없는 `**`를 남겼다** — `lifecycle-pattern.md`의 `**` 개수가
144(짝수)→145(홀수)로 바뀐 걸 감사자가 교차 확인해 잡았다. 렌더러에 따라
그 뒤 리스트의 볼드 페어링까지 밀려 깨질 수 있는 실제 결함이었다. 복구한 뒤
변경 파일 전체에 같은 검사를 기계적으로 돌려, 내 변경이 짝을 뒤집은 파일은
그것 하나뿐임을 확인했다(다른 홀수 파일들은 HEAD에서도 이미 홀수 — 코드블록
안의 `**` 때문).
**구조적 사각지대 하나가 같이 드러났다**: `- **[해소됨, …]** 실제 제목 — …`
처럼 **볼드가 상태 태그에서 닫히고 진짜 제목이 볼드 밖 평문**이면
(`archive/question-resolved.md`가 이 형태) 검사기는 `[해소됨, …]`만 절
제목으로 본다. 절 참조가 ERROR로 승격된 뒤라, 이걸 모르고 규약대로 큰따옴표
인용을 하면 **실재하는 서술인데도 커밋이 막힌다.** 규약에 캐비엇으로 명시하고
"새로 쓸 땐 태그와 제목을 한 볼드 안에" 지침을 넣었다. 게다가 이 세션이
신설한 "세션은 파일 ID로 지칭" 관례가 `**[태그, 세션ID]**` 형태의 리드를
늘리는 방향이라 재생산될 구조였다는 지적까지 붙었다.
보류한 것 하나 — `#` 헤딩 검사가 **부분문자열**이라 `"확정"` 같은 짧은
인용은 같은 파일의 무관한 헤딩에 걸려 통과한다(`slot-plan.md`엔 "확정"이 든
헤딩이 6개). 다만 감사가 **실제 오매칭 사례를 하나도 못 찾았고**, 사용자와
같이 `conventions.md`의 "드문 오용이나 가상의 미래 요구까지 방어/최적화하려고
구조를 복잡하게 만들지 않는다" 원칙에 따라 지금은 손대지 않기로 했다.
## 감사자가 `git stash`를 쓴다 — 실동에서 드러난 위험
`.pyc` 추적 해제(`git rm --cached`)가 두 번 되돌아갔다. 원인은 감사자들이
HEAD와 대조하려고 `git stash`를 건 것 — **커밋 안 된 작업 트리에서** 그걸
하면 메인 세션의 스테이지가 풀리고, pop이 실패하면 세션 작업을 통째로 날릴
수 있다. 이번엔 유실이 없음을 확인했고, `.claude/agents/quad-doc-auditor.md`
작업 트리를 바꾸는 git 명령 금지와 대안(`git show HEAD:<경로>` 등)을 규약으로
넣었다. "읽기 전용"을 도구 목록으로 못 거는 이상(`tools:` 필드가 그대로
반영되지 않는 기존 미해결 항목) 이런 규약이 유일한 방어선이다.

View file

@ -164,3 +164,29 @@
diff해서 대조했고, 그 구간에 오히려 절 참조 오류 2건이 해소된 것도 diff해서 대조했고, 그 구간에 오히려 절 참조 오류 2건이 해소된 것도
확인됨. M0/설계 게이트와 무관. 확인됨. M0/설계 게이트와 무관.
8. **[2026-08-16 신설, 이미 닫힘 — 다음 세션이 알아야 할 규약]** 절 인용
규약이 생겼다. 이제 `` `<파일>.md`의 "절 제목" `` 형태로 인용할 땐
**의역하지 말고 원문에서 잘라 쓸 것**(`#` 헤딩은 부분문자열, `**볼드**`
절은 줄머리 + 앞부분일치). 규칙 본문은 `.claude/conventions.md`
"절 인용 규약"이 소스 — 여기서 반복하지 않음. 지키지 않으면
`doc-check.py`가 **ERROR**로 잡아 커밋 게이트에 걸린다(WARN이 아님 —
절 참조 불일치를 78→0으로 정리한 뒤 승격했음). 경위는
`session/2026-08-16-03-doc-check-section-convention.md`.
9. **[2026-08-16 신설, 이월 — 급하지 않음]** 이번 절 인용 규약 작업에서
의도적으로 **안 한** 것 둘. 둘 다 다음 세션이 알아야 이중 조사를 안 한다.
- **`#` 헤딩 검사가 부분문자열이라 느슨하다.** `"확정"` 같은 짧은 인용은
같은 파일의 무관한 헤딩에 걸려 통과한다(`base/slot-plan.md`엔 "확정"이
든 헤딩이 여러 개). 커밋 전 감사가 **실제 오매칭 사례를 하나도 못
찾았고**, `conventions.md`의 "드문 오용이나 가상의 미래 요구까지
방어/최적화하려고 구조를 복잡하게 만들지 않는다" 원칙에 따라 지금은
안 고치기로 사용자와 합의. 실제로 물리면 그때 좁힐 것(길이 하한, 후보
2개 이상이면 WARN 등).
- **⚠️ 감사자에게 `git stash`를 쓰지 말라고 프롬프트에도 매번 적을 것.**
커밋 안 된 작업 트리에서 감사자가 HEAD 대조하려고 stash를 걸어 메인
세션의 스테이지가 반복적으로 풀렸다(2026-08-16 실동, 유실은 없었음).
금지 규약을 `.claude/agents/quad-doc-auditor.md`에 넣어두긴 했지만
**정의 파일이 언제 반영되는지 모른다는 게 위 7번의 미해결 1번**이라,
정의에만 의존하지 말고 감사자를 띄우는 프롬프트에서 직접 금지할 것.
대안은 `git show HEAD:<경로>` / `git diff HEAD -- <경로>`.

View file

@ -16,10 +16,15 @@
1. [ERROR/WARN] 깨진 파일 참조 라이브 문서가 가리키는 .md/.luau가 실제로 1. [ERROR/WARN] 깨진 파일 참조 라이브 문서가 가리키는 .md/.luau가 실제로
없음. 파일명이 레포의 명명 관례(`OURS`) 걸리면 ERROR, 아니면 없음. 파일명이 레포의 명명 관례(`OURS`) 걸리면 ERROR, 아니면
외부 문서명일 있어 WARN. 외부 문서명일 있어 WARN.
2. [WARN] 깨진 참조 `foo.md` "절 제목" 파일에 없음 2. [ERROR] 깨진 참조 `foo.md` "절 제목" 파일에 없음
(문서를 쪼개거나 헤딩을 고칠 가장 걸리는 아홉 번째 세션에 (문서를 쪼개거나 헤딩을 고칠 가장 걸리는 아홉 번째 세션에
bind-system-plan.md를 분할하며 20곳이 여기 걸렸음. 제목을 의역해 bind-system-plan.md를 분할하며 20곳이 여기 걸렸음)
인용하는 관례가 있어 오탐이 섞이므로 ERROR가 아니라 WARN) [2026-08-16] 판정 규칙은 `BOLD_LEAD` 주석과 `conventions.md`
"절 인용 규약" 소스 `#` 헤딩은 부분문자열, `**볼드**` 절은 줄머리 +
앞부분일치. 예전엔 `#` 헤딩만 읽어서 볼드 절 인용을 전부 오탐으로
냈고(78 30), "의역 인용 관례 때문에 오탐이 섞인다" WARN에
묶여 있었다. 의역 인용을 규약으로 금지하고 불일치를 780으로 정리한
ERROR로 승격함.
3. [ERROR] 색인 누락 base/research/archive/reference 파일이 README에 없음 3. [ERROR] 색인 누락 base/research/archive/reference 파일이 README에 없음
4. [WARN] 날짜 없는 시한부 주장 "아직 안 돌려봄", "열린 질문 없음" 4. [WARN] 날짜 없는 시한부 주장 "아직 안 돌려봄", "열린 질문 없음"
시간이 지나면 거짓이 되는데 언제 기준인지 적힌 문장 시간이 지나면 거짓이 되는데 언제 기준인지 적힌 문장
@ -74,7 +79,9 @@ def rel(p):
# 예전엔 줄 단위로 돌려서 `base/\nbind-system-plan.md`의 "Length/Offset" 같은 # 예전엔 줄 단위로 돌려서 `base/\nbind-system-plan.md`의 "Length/Offset" 같은
# 자연스러운 줄바꿈 인용을 통째로 놓쳤고, 실제로 문서 분할 후 stale 참조 # 자연스러운 줄바꿈 인용을 통째로 놓쳤고, 실제로 문서 분할 후 stale 참조
# 20여 곳이 이 사각지대로 빠져나갔음(14차 세션 리뷰에서 발견). # 20여 곳이 이 사각지대로 빠져나갔음(14차 세션 리뷰에서 발견).
REF = re.compile(r'`\.?/?((?:[\w.-]+/\s*)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"]{2,60})")?') # [2026-08-16] 인용 길이 상한을 60→160자로 넓힘. 60자를 넘는 절 인용은 매치
# 자체가 안 걸려 **검사에서 조용히 빠져나갔다** — 위양성보다 나쁜 종류의 구멍.
REF = re.compile(r'`\.?/?((?:[\w.-]+/\s*)*[\w.@-]+\.(?:md|luau))`(\s*(?:의\s*)?"([^"]{2,160})")?')
def resolve(target, src): def resolve(target, src):
"""참조 문자열을 실제 경로로 해석 — 상대/부분 경로를 관대하게 매칭.""" """참조 문자열을 실제 경로로 해석 — 상대/부분 경로를 관대하게 매칭."""
@ -108,6 +115,52 @@ OURS = re.compile(r'(-plan|-reversed|-rejected|-findings|-map|-audit|-verificati
r'|^conventions|^project-context|^todos|^session-summary)\.md$') r'|^conventions|^project-context|^todos|^session-summary)\.md$')
# [2026-08-16] 이 코퍼스의 "절"은 `#` 헤딩만이 아니다 — `**볼드**`로 시작하는
# 줄을 하위 절로 쓰고, 다른 문서들이 그걸 "절"이라 부르며 인용해왔다. 그걸
# 안 읽어서 절 참조 WARN 상당수가 위양성이었음 — 라운드별 실측 수치는
# `session/2026-08-16-03-doc-check-section-convention.md`가 소스(여기 복제 안 함).
#
# 다만 볼드를 통째로 절 제목으로 인정하면 검사가 장식이 된다 — 실측상 볼드는
# `#` 헤딩보다 압도적으로 흔하고 대부분은 그냥 강조다. 인용이 무관한 강조
# 스팬에 우연히 걸리면 검사가 조용히 통과해버려 진짜 rot을 놓친다. 그래서
# 규칙을 비대칭으로 둔다:
# - `#` 헤딩 : **부분문자열** 인용 허용 (명시적 절 마커라 오인 위험 없음)
# - `**볼드**` 절: 빈 줄 다음이나 리스트 항목 머리에 있고, 인용이 제목
# **앞부분부터**일 때만 인정
# 이 비대칭이 곧 `conventions.md`의 "절 인용 규약"이다. 느슨한 규칙과 해소
# 건수는 같으면서 매칭 표면만 좁다는 걸 실측으로 확인하고 채택했다.
# 절 제목으로 인정하는 볼드는 **빈 줄 다음** 또는 **리스트 항목**뿐이다.
# 그냥 "줄머리 볼드"로 두면 **문단이 줄바꿈되며 우연히 줄머리에 걸린 강조**까지
# 절로 잡힌다 — `architecture.md`의 `**pluggable 디스패치 엔진 자체도 …**`가
# 실제 사례로, 진짜 절은 두 줄 위의 `**패키지 경계**`인데 두 문서가 그 우연한
# 자리를 절이라며 인용하고 있었다(커밋 전 감사가 잡음).
# 리스트 마커는 `-`/`*`/`>`뿐 아니라 `1.`도 인정 — `conventions.md`의 핸드오버
# 체크리스트처럼 **번호 항목 자체가 절**이고 다른 문서가 그걸 인용하는 형태가 있다.
BOLD_LEAD = re.compile(
r'(?:\A|\n[ \t]*\n)[ \t]{0,6}\*\*(.+?)\*\*' # 빈 줄 다음
r'|(?:\A|\n)[ \t]{0,6}(?:[-*>]|\d+\.)[ \t]+\*\*(.+?)\*\*', # 리스트 항목
re.S)
# 선두 장식(⚠️/⭐ 등)은 인용할 때 관례적으로 생략되므로 양쪽에서 벗겨내고 비교
DECOR = re.compile(r'^[^\w가-힣`(\[]+')
# 선두 상태/날짜 태그(`[2026-08-16 신설]`, `[해소됨, …]`)도 제목의 일부가 아니다 —
# 시간이 지나면 내용이 바뀌는 메타 표시라 여기에 인용을 묶으면 태그가 갱신될 때마다
# 참조가 깨진다. 양쪽에서 똑같이 벗겨내므로 태그째 인용해도 여전히 매칭된다.
TAG = re.compile(r'^\[[^\]]{2,80}\]\s*')
def norm_section(s):
"""절 제목/인용문을 비교 가능한 형태로 정규화."""
s = re.sub(r'\s+', ' ', re.sub(r'[`*]', '', s)).strip()
s = DECOR.sub('', s).strip()
stripped = DECOR.sub('', TAG.sub('', s)).strip()
return stripped or s # 태그가 제목 전부였다면 원본을 남긴다
def squash(s):
"""비교 전용 — 정규화 후 공백까지 전부 제거(줄바꿈 인용 대응)."""
return re.sub(r'\s+', '', norm_section(s))
def headings(path): def headings(path):
if not path.endswith('.md') or not os.path.exists(path): if not path.endswith('.md') or not os.path.exists(path):
return None return None
@ -115,10 +168,18 @@ def headings(path):
for line in open(path, encoding='utf-8'): for line in open(path, encoding='utf-8'):
if line.startswith('#'): if line.startswith('#'):
# 백틱/강조 표기는 인용할 때 자주 빠지므로 정규화해서 비교 # 백틱/강조 표기는 인용할 때 자주 빠지므로 정규화해서 비교
hs.append(re.sub(r'[`*]', '', line.lstrip('#')).strip()) hs.append(norm_section(line.lstrip('#')))
return hs return hs
def bold_leads(path):
"""줄머리 `**볼드**` 절 제목. 위 BOLD_LEAD 주석의 비대칭 규칙 참고."""
if not path.endswith('.md') or not os.path.exists(path):
return []
txt = open(path, encoding='utf-8').read()
return [norm_section(a or b) for a, b in BOLD_LEAD.findall(txt)]
def interesting(target): def interesting(target):
"""검사 가치가 있는 참조인가. """검사 가치가 있는 참조인가.
@ -170,14 +231,27 @@ def check_refs(docs):
(errors if OURS.search(name) else warns).append( (errors if OURS.search(name) else warns).append(
msg if OURS.search(name) else msg + " (외부 문서명일 수 있음)") msg if OURS.search(name) else msg + " (외부 문서명일 수 있음)")
continue continue
if section and not is_archive and p.endswith('.md'): # `initreq/`를 가리키는 인용은 절 검사 제외 — 읽기 전용 외부 원본이라
# 절 구조가 없다(`raw-userinput.md`는 스스로 "정리가 없는 생각의 흐름"
# 이라고 밝힌 원문). 저기로 가는 인용은 절 제목이 아니라 원문 문장을
# 따오는 것이므로 헤딩과 대조하는 게 애초에 의미가 없다.
if section and not is_archive and p.endswith('.md') \
and 'initreq' not in rel(p).split(os.sep):
if p not in hcache: if p not in hcache:
hcache[p] = headings(p) or [] hcache[p] = (headings(p) or [], bold_leads(p))
core = re.sub(r'[`*]', '', section).strip() heads, leads = hcache[p]
if not any(core in h for h in hcache[p]): # 공백을 뺀 형태로 비교 — 인용이 줄바꿈에 걸리면 원문엔 없는
# 절 제목은 의역해서 인용하는 관례가 있어 WARN — 다만 # 공백이 끼어든다(`canExecute/\nunbindLifetime` → "canExecute/
# 문서를 쪼개거나 헤딩을 고칠 때 여기가 제일 먼저 걸린다. # unbindLifetime"). 이것 때문에 실재하는 절이 불일치로 잡혔음.
warns.append( core = squash(section)
if not (any(core in squash(h) for h in heads)
or any(squash(s).startswith(core) for s in leads)):
# [2026-08-16] WARN → ERROR로 승격. 예전엔 "절 제목을
# 의역해 인용하는 관례" 탓에 오탐이 섞여 WARN이었지만,
# `conventions.md`의 "절 인용 규약"이 의역을 금지하고
# 불일치를 78→0으로 정리한 뒤 게이트로 올렸다. 문서를
# 쪼개거나 헤딩을 고칠 때 여기가 제일 먼저 걸린다.
errors.append(
f"{rel(d)}:{ln} 절 참조 불일치 → {os.path.basename(p)}" f"{rel(d)}:{ln} 절 참조 불일치 → {os.path.basename(p)}"
f'"{section}" 절 없음') f'"{section}" 절 없음')

4
.gitignore vendored
View file

@ -1 +1,5 @@
.claude/initreq .claude/initreq
# Python 바이트코드 — doc-check.py 실행 시 생김(32e9db0에 실수로 딸려 들어갔었음)
__pycache__/
*.pyc

View file

@ -119,8 +119,9 @@ B(Attribute의 Instance 참조 타입)/C(CollectionService 태그 왕복) —
2. 사용 가능해지는 순간 에이전트에 알려줄 것. 2. 사용 가능해지는 순간 에이전트에 알려줄 것.
둘 중 어느 쪽이든 **에이전트는 스스로 판단하지 않고 대기**한다 — 알려주기 둘 중 어느 쪽이든 **에이전트는 스스로 판단하지 않고 대기**한다 — 알려주기
전까지는 `architecture.md`의 "새로 짜는 코드는 일단 `local`로" 원칙을 그대로 전까지는 `architecture.md`의 "`const` 바인딩도 Luau 공식 문법" 절이 정한
따름. 알려주면 그때 `architecture.md`의 해당 절을 갱신하고 기존 코드의 "새로 짜는 코드는 일단 `local`로" 원칙을 그대로 따름. 알려주면 그때
`architecture.md`의 해당 절을 갱신하고 기존 코드의
`const` 전환 범위를 같이 상의할 것. `const` 전환 범위를 같이 상의할 것.
## 6. **[2026-08-13 신설, 안 막음]** 에디터의 Luau 솔버 설정 확인 ## 6. **[2026-08-13 신설, 안 막음]** 에디터의 Luau 솔버 설정 확인

View file

@ -353,9 +353,9 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
> **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료** — 아래 > **✅ [2026-08-13 열네 번째 세션] 재디스패치 모델 교체 완료** — 아래
> "`SlotHandler.process`는 claim 실패 시에도 파괴적 클로저를 반환해야 함" > "`SlotHandler.process`는 claim 실패 시에도 파괴적 클로저를 반환해야 함"
> 항목은 새 모델에서도 그대로 유효함(체인은 클로저를 early-return > 항목은 새 모델에서도 그대로 유효함(체인은 클로저를 early-return
> 여부와 무관하게 항상 소비 — `base/dispatch-core-plan.md` "Handler 작성 > 여부와 무관하게 항상 소비 — `base/dispatch-core-plan.md`
> 체크리스트" 1번). 클로저가 받는 값이 항상 `Slot`이거나 `nil`임이 > "Handler 작성 체크리스트" 1번). 클로저가 받는 값이 항상 `Slot`이거나
> 계약으로 보장된다는 점만 새로 추가됨. > `nil`임이 계약으로 보장된다는 점만 새로 추가됨.
- [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]** - [ ] **[2026-08-13 여섯 번째 세션 — 이 세션의 Slot 결정 전부, 구현 전 필독]**
- **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일). - **`State<Slot>` 교체 = 파괴가 아니라 언마운트**(`state<Frame>`와 동일).
@ -469,13 +469,13 @@ Luau 코드로 부딪혀본 적 없는 세 가지**를 던지는 코드로 검
기각(Slot 부모 자체가 Destroy되는 경로에선 이 훅이 전혀 안 불려서 기각(Slot 부모 자체가 Destroy되는 경로에선 이 훅이 전혀 안 불려서
절반만 동작, `retract`가 Destroy 시 안 불리는 것과 같은 이유). 절반만 동작, `retract`가 Destroy 시 안 불리는 것과 같은 이유).
(2026-08-09 세 번째 세션 확정, `offset`/raw `index`/세 갈래 구조는 (2026-08-09 세 번째 세션 확정, `offset`/raw `index`/세 갈래 구조는
2026-08-11 세션 추가 확정, `base/slot-plan.md` "`Slot:List(...)`" 절) 2026-08-11 세션 추가 확정, `base/slot-plan.md` "`Slot:List(data, updateFn, keyFn?)`" 절)
구현. 구현.
**`data:Observer(fn)` 구독은 `:List()` 호출 시점이 아니라 Slot **`data:Observer(fn)` 구독은 `:List()` 호출 시점이 아니라 Slot
마운트 시점까지 lazy — `Dispatch.setLength`와 같은 패턴으로 마운트 시점까지 lazy — `Dispatch.setLength`와 같은 패턴으로
`bindLifetime(inst,observer)`(마운트 이후 `:List()`가 불리면 `bindLifetime(inst,observer)`(마운트 이후 `:List()`가 불리면
`self._mounted` 확인 후 즉시 활성화)** (2026-08-09 일곱 번째 세션, `self._mounted` 확인 후 즉시 활성화)** (2026-08-09 일곱 번째 세션,
`base/slot-plan.md` "`Slot:List(...)`"의 "구독 시점" 절) `base/slot-plan.md` "`Slot:List(data, updateFn, keyFn?)`"의 "구독 시점" 절)
**`Slot.Offset: Source<number>``Slot.Length`처럼 공개 필드로 **`Slot.Offset: Source<number>``Slot.Length`처럼 공개 필드로
노출 — Slot 마운트 시점에 `Dispatch.setOffsetSource`가 등록하는 노출 — Slot 마운트 시점에 `Dispatch.setOffsetSource`가 등록하는
바로 그 Source를 `self.Offset`으로도 저장**(2026-08-11 세션, 바로 그 Source를 `self.Offset`으로도 저장**(2026-08-11 세션,