quad/.claude/session/2026-08-14-11-corpus-audit-canbound-resplit.md
qwreey e35905ceca
docs: 코퍼스 전체 감사 + canBound/canExecute 재분리, Ref 0-W 해소
전체 .claude/ 코퍼스를 doc-check.py + 6개 병렬 서브에이전트로 감사해
stale 세션 번호, 잘못된 인용, 자기모순 배너 등 15개 파일의 실제 사실
오류를 발견·수정. 이어서 question.md 0-W(같은 Ref 객체가 두 자리에
동시에 놓이는 문제)를 선택지 (a)로 확정 — RefLeafHandler가 새 Relate
없이 bindLifetime/unbindLifetime을 재사용해 이중 배치를 방지.

이 과정에서 bindLifetime의 이중 바인딩 가드(bound 문맥)와 State emit
전파 게이팅(execute 문맥)이 서로 다른 질문인데 canExecute 하나로
뭉쳐 있던 걸 발견 — canBound를 별도 진입점으로 재도입(판정 로직은
비공개 헬퍼 하나를 공유, 코드 중복 없음). Tag/Attribute의 미지원
백엔드 처리 모델도 여러 라운드 논의 끝에 확정: TagHandler 등은
quad-base가 스스로 등록하고, addTag/removeTag/setAttribute만 백엔드
팩토리가 채우는 타입 계약 — 안 채운 슬롯은 명시적으로 에러내는 스텁.

/code-review high가 추가로 3건(gcconn-trick-verification.md 배너,
README.md 인덱스 4곳, luau-test/STATUS.md의 재작성 지침) 발견해 정정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXqrGrh83Sh61C3tUCMdmY
2026-08-14 11:03:24 +09:00

19 KiB

2026-08-14 열한 번째 세션 — 코퍼스 전체 감사(서브에이전트 6개 병렬) + canBound/canExecute 재분리, question.md 0-W 해소

코퍼스 감사

사용자 요청으로 .claude/ 전체를 감사 — 먼저 python3 .claude/tools/doc-check.py로 기계 점검(ERROR 0 확인)한 뒤, base/(3분할)·research/+reference/· luau-test/+audit/+archive/·루트 인덱스 파일(CLAUDE.md/ROADMAP.md/ HUMAN_TODO.md/question.md/.claude/README.md) 6개 영역을 서브에이전트로 병렬 감사, 보고된 항목을 직접 재확인 후 실제 사실 오류만 반영. 총 15개 파일 수정:

  • modifier-plan.md 2곳 — Brand/isState 정의 인용이 9차 세션 문서 분할 후 옛 경로(bind-system-plan.md)를 계속 가리킴 → brand-plan.md로 정정
  • tag-plan.mdTagHandler.priority 의사코드가 HANDLER_PRIORITY_FALLBACK 이어야 하는데 <일반>으로 남아 같은 문서의 "패키지 배치" 절과 자기모순
  • typing-limits.md — 스파이크 경로가 review-required/08로 stale(13차 세션에 done/08로 이동)
  • store-plan.mdstore.key 타입함수 접근을 "실측 확인"처럼 과장 서술해 typing-limits.md(스파이크 16은 여전히 rewrite-required/, 미검증)와 모순 → caveat 추가
  • additional-primitives-plan.md — "새로 열린 질문 없음" 배너가 이후 세션에 추가된 열린 질문 2개(Attribute unset 유틸, State<State<T>> 평탄화)와 자기모순
  • comparison-charm.md+.claude/README.md — "quad가 미결로 남긴 previous 값 비교 문제"라며 존재하지 않는 문서(additional-primitives-plan.md)를 잘못 인용 — 실제로는 source-state-plan.md에 이미 확정된 :Computeprevious 인자
  • luau-test/STATUS.md/README.mdcanExecute 재정정 세션 번호가 "세 번째"(STATUS.md 헤더 1곳, README.md 2곳)와 "다섯 번째"(같은 파일 본문, archive, CLAUDE.md)로 갈라짐 — archive 3곳 교차확인 결과 "다섯 번째"가 맞음, 05(8차 세션 이동)도 README 테이블에 누락돼 있어 보강
  • question.md/HUMAN_TODO.md — 0-W가 "M4 구현 세부만 막음"이라 서술했는데 실제 Ref 구현은 M8 — 정정, ROADMAP.md M8에 포인터 추가
  • question.md — Debounce/Throttle "남은 열린 질문 4개"가 실제 debounce-throttle-plan.md 12절엔 6개(항목 3/7 누락) → 개수 서술 제거하고 소스 문서로 위임
  • CLAUDE.md 자기 자신 2곳 — luau-test/rewrite-required 나열이 05(8차 세션 합류)/04/19(14차 세션 합류)를 빠뜨려 stale, "지금 할 일" 절엔 "더 이상 대기 항목 아님"이 바로 위 문단과 자기모순 → 나열 자체를 걷어내고 STATUS.md로 위임

doc-check.py ERROR 0 유지 확인. 발견 없음으로 보고된 영역: architecture/attribute/bind-system/blocker/brand/ component-composition/dispatch-core/effect/event/fallback/ lifecycle-hooks/lifecycle-pattern(아래 별도 발견 있었음, 감사 라운드 자체는 통과 보고했으나 이후 논의 중 직접 발견)/module-lifecycle/ onchange/purity-and-effects/ref/relate/source-state/tween/ ui-shorthand-plan, debounce-throttle/debug-tooling/ documentation-*/framework-comparison/operator-sugar/ pre-implementation-audit/v1-compat-plan, comparison-fusion-vide, quad-v1-architecture, archive/ 23개 전체, audit/ 나머지.

canBound/canExecute 재분리, question.md 0-W 해소

감사 완료 후 사용자가 question.md 0-W(같은 Ref가 두 자리에 동시에 놓이는 걸 막을지)에 대해 이미 마음속으로 결정해뒀던 것을 풀어놓음 — 선택지 (a)(즉시 error) 확정, 메커니즘은 RefLeafHandlerbindLifetime/ unbindLifetime을 재사용(새 전용 Relate 불필요 — bindLifetime이 이미 내장한 이중 바인딩 가드를 그대로 타면 됨). 그런데 이 가드가 지금 canExecute라는 이름으로 되어 있는 게 이상하다는 지적 — canExecute는 "emit 처리로 State 전파 루프가 Effect/Observer를 발화시켜도 되는가"를 묻는 함수인데, Ref는 emit 전파에 참여조차 안 하니 그 이름을 쓰는 게 개념적으로 안 맞음. "bound 문맥"(이미 묶여 있는가 — bindLifetime/ Observer:Subscribe()의 이중 등록 가드가 묻는 것)과 "execute 문맥"(지금 발화해도 되는가 — State 전파 루프만 묻는 것)은 오늘 판정값이 같아도 서로 다른 질문이라는 게 사용자 논거.

해법(사용자 제안) — 이름은 둘, 판정 로직은 하나. 2026-08-14 다섯 번째 세션이 "canBound 폐기, canExecute로 통합"했던 걸 부분적으로 되짚어 canBound를 별도 진입점으로 재도입하되, 실제 gcconn/.Subscribed 체크는 비공개 헬퍼 isBoundAlive(value) 하나에만 두고 canBound/canExecute 둘 다 그 헬퍼를 부르는 얇은 래퍼로 — 코드 중복 없이 호출부 의미만 분리. 다섯 번째 세션이 고친 것(2-인자 canExecute(inst,value)가 오염이었다는 것, unbindLifetime/canExecuteinst를 안 받아야 한다는 것)은 전부 그대로 유효 — 되짚은 건 "합칠지 나눌지"라는 좁은 하위 결정뿐.

반영: base/lifecycle-pattern.md(정본, isBoundAlive/canBound/ canExecute 코드 전면 재작성 + "canBound vs canExecute" 절 신설), base/ref-plan.md("이중 배치 방지" 절 신설, RefLeafHandler 의사코드에 bindLifetime/unbindLifetime 호출 추가), archive/ canexecute-inst-arg-reversed.md(재분리 경위 addendum), question.md 0-W 항목을 archive/question-resolved.md로 이전, ROADMAP.md M2/M3/M8 체크리스트, base/source-state-plan.md의 "이중 바인딩 금지" 절(게이트를 canBound로 교체), base/effect-plan.md(UB 절 갱신 + "세 번째 세션" 오표기를 "다섯 번째"로 같이 정정 — lifecycle-pattern.md에서 도 같은 오표기 발견해 정정), base/architecture.md(소스 트리 주석), base/dispatch-core-plan.md/research/pre-implementation-audit.md (절 제목 인용 갱신), luau-test/README.md/audit/ gcconn-trick-verification.md(스파이크 10 재작성 시 반영할 새 게이트 이름 갱신), CLAUDE.md 자신("지금 할 일" 0번 — question.md의 "결정 대기" 절이 이제 완전히 빔).

doc-check.py ERROR 0 유지. 새로 연 설계 질문 없음 — SlotclaimOwner/Attribute의 이름 claim처럼 Ref도 기존 프리미티브 (bindLifetime)를 재사용하는 것으로 끝남.

후속 — Frame1{Ref=r} 표기 정정, PreRef/PostRef/Observer/Effect

동적 경로 가드를 HANDLER_PRIORITY_FALLBACK으로 통일

사용자가 0-W 손 트레이싱의 Frame1 { Ref = r } 표기를 질문 — Ref는 항상 children 배열 리터럴로만 놓이므로 k가 문자열 "Ref"일 리 없고(실제로는 배열 인덱스, 숫자), 순수 설명 편의 표기였음을 확인. base/ref-plan.md의 새 절과 archive/question-resolved.md에 보존된 원본 트레이싱에 정정 각주 추가(원본 자체는 역사 보존 목적으로 안 지움).

이어서 사용자가 PreRef/PostRef/Observer/Effect가 non-number 키(해시 파트 등 동적 경로)로 들어오면 어떻게 할지 질문 — 처음엔 "k 무관하게 매치해서 process에서 에러"(Attribute처럼 process 도중 에러내는 기존 패턴)를 제안했다가, 곧바로 스스로 "가장 아래(FALLBACK)에 까는 게 맞다"고 정정. 결론: PreRef의 기존 "동적 경로 가드" Handler가 이미 k 무관 매치+에러였지만 우선순위가 명시돼 있지 않았던 걸 HANDLER_PRIORITY_FALLBACK으로 확정 — 하드 블록이 아니라 Tag/ Attribute와 같은 "base가 소유하되 평범한 우선순위의 다른 Handler가 있으면 그쪽이 이기는" 자리로 만들어, 지금은 항상 에러가 나지만 나중에 named 자리 바인드 같은 실제 기능이 확정되면 base 가드를 안 건드리고 그 기능의 Handler만 등록하면 자동으로 우선하게 됨. 같은 패턴을 PostRef (기존 거울상 가드에 우선순위 추가)와 Observer/Effect(기존엔 전용 가드가 없어 범용 "매치 실패=에러" 경로로 우연히 같은 결과를 내고 있었음 — 이번에 전용 HANDLER_PRIORITY_FALLBACK 가드를 신설해 넷 다 명시적으로 통일, 동작 자체는 안 바뀌고 에러 메시지만 명확해짐)에도 적용. base/ref-plan.md/base/source-state-plan.md/base/effect-plan.md/ ROADMAP.md M8/M3 반영, doc-check.py ERROR 0 유지.

이어서 사용자가 Tag/Attribute의 미주입 백엔드 실패 모드(base 기본 스텁이 명확한 에러를 낸다는 것)를 확인한 뒤, "미지원"과 "미등록"을 구분 안 하는 게 맞는지 질문 — 확정 원칙(pre-implementation-audit.md 1-4, 스텁 입장에선 원천적으로 구별 불가) 그대로 재확인. 사용자가 "정말로 미지원인 백엔드는 FALLBACK보다 높은 우선순위로 자기 Handler를 등록해 명시적으로 에러내면 된다"는 관례를 문서에 말로만 적어두자고 제안(강제 메커니즘 아님, 대부분 백엔드가 결국 지원할 거라 실익은 크지 않지만 opt-in 옵션으로) — base/dispatch-core-plan.md의 "base가 소유하는 핸들러와 주입되는 엔진 op" 절에 관례 bullet 추가.

곧바로 더 단순한 안으로 정정: 사용자가 "그런 환경은 addTag든 뭐든 nop일 수도 있으니, [모든 백엔드가] 이 op들을 다 등록하도록 해야 하고, 대신 [미지원이면] 에러로 '구현 안 되어있다'를 내도록 [팩토리가 직접 작성]하면 된다"고 제시 — 방금 추가한 "Handler 우선순위로 덮어씌우기" 관례를 철회하고, 대신 op 등록 자체를 필수로 바꿈: 백엔드 팩토리는 addTag/removeTag/setAttribute를 항상 명시적으로 채워야 하고(빈 자리로 두고 base의 미주입 스텁에 기대지 않음), 지원 안 하는 op은 nop(정당한 구현 선택)이든 function() error("구현 안 됨") end(명시적 에러)든 팩토리가 직접 등록. Dispatch Handler나 우선순위 조정이 전혀 불필요해서 이전 안보다 훨씬 가벼움 — 부수 효과로 "provider 미주입"과 "미지원"도 자연히 갈라짐: 제대로 된 팩토리는 항상 세 op를 다 채우므로, base 스텁이 실제로 매치되는 건 "어떤 팩토리도 안 돌았다"는 훨씬 좁은 경우로 좁혀짐. base/dispatch-core-plan.md/base/module-lifecycle-plan.md 반영, doc-check.py ERROR 0 유지.

다시 한 번 정정 — 사용자가 이번엔 op 필수화만으론 부족하다고 지적. addTag 에러는 TagHandler.process 본문 안, 즉 tagNameMap 같은 부기가 이미 일부 mutate된 에야 남 — Tag()가 "바인드는 됐는데 엔진 반영만 실패"인 반쪽짜리 상태로 남을 수 있어 원자적 실패가 아님. 사용자 결론: "그래도 여전히 Tag()는 바운드 가능함... 핸들러도 1만큼 높은 우선순위 채워주는 게 가장 안전하고 사실상 무료" — op 필수화(위)는 유지하되, 추가로 미지원 백엔드는 HANDLER_PRIORITY_FALLBACK + 1 우선순위의 순수 가로채기 Handler(isHandlable = isTag(v), process = error(...))도 등록해 TagHandler.process 자체가 아예 안 불리게(부기 mutation 0회) 하는 걸 권장으로 추가 — 매치된 Handler 하나만 실행되는 기존 Dispatch 규칙을 그대로 이용하는 것이라 새 메커니즘 없음. base/dispatch-core-plan.md/ base/module-lifecycle-plan.md 재반영, doc-check.py ERROR 0 유지.

세 번째 정정 — 사용자가 "철회 아님, 전제 자체가 틀렸다"고 바로잡음. 어시스턴트가 "TagHandler/AttributeKeyHandler는 quad-base가 자기 모듈 로드 시점에 스스로 등록한다"고 잘못 전제하고 "+1 Handler" 안을 철회했었는데, 사용자가 정정: Tag/Attribute는 모든 백엔드의 필수 구현이 아니고, quad-base는 이 Handler들을 완성된 값으로 export만 할 뿐 스스로 Dispatch에 등록하지 않는다. 백엔드 팩토리가 둘 중 하나를 선택: (A) 지원함 — quad-base Handler를 그대로 등록 + addTag/ removeTag/setAttribute 실제 구현 주입, (B) 지원 안 함 — 그 Handler는 등록하지 않고 HANDLER_PRIORITY_FALLBACK + 1 우선순위의 얇은 "미지원" 가로채기 Handler만 등록(op 주입 자체가 불필요 — 호출할 주체가 그 백엔드의 레지스트리에 없음). 이 모델이면 "op 에러가 부기 mutation 뒤에 지연 발생"하는 문제 자체가 안 생김 — (B)를 고른 백엔드에서 부기를 mutate하는 TagHandler.process가 아예 안 불리기 때문. 앞서 검토했던 "op 필수화"와 "process 재정렬" 두 안은 전부 이 등록-선택 모델로 대체되어 불필요해짐. base/dispatch-core-plan.md(정본, 전면 재작성)/base/module-lifecycle-plan.md/base/tag-plan.md/ base/attribute-plan.md/base/architecture.md 반영, doc-check.py ERROR 0 유지.

네 번째, 최종 정정 — "등록-선택 모델" 자체가 틀렸음, 사용자가 바로잡음. 사용자: "HANDLER_PRIORITY_FALLBACK까지 등록 안한다 했는데, 개소리 ㄴㄴ HANDLER_PRIORITY_FALLBACK는 아무 등록 없는데 처리하려 할 때 방어까지 포함하는 애임. 기본 등록은 필요함." 최종 확정 모델: TagHandler/ AttributeKeyHandler/AttributeGroupHandler는 quad-base가 모듈 로드 시점에 HANDLER_PRIORITY_FALLBACK으로 스스로 등록(모든 백엔드가 자동으로 부기를 얻음, FALLBACK 밴드의 존재 이유 자체가 이 "기본 안전 동작 제공"임). addTag/removeTag/setAttribute만 백엔드 팩토리가 채우는 타입 계약이고, 안 채운 슬롯의 base 기본값은 "그럴듯한 기본 동작을 추측"하지 않고 명시적으로 에러내는 스텁(조용한 no-op은 provider 초기화를 잊은 실수를 가려버려서 기각). 더 명확한 메시지나 진짜 원자적 실패(부기 mutation 0회)를 원하는 백엔드만 opt-in으로 HANDLER_PRIORITY_FALLBACK + 1짜리 가로채기 Handler를 추가로 등록— 이게 필수 요구사항이 아니라 선택적 업그레이드라는 게 핵심(base 기본 스텁 하나로도 AttributeGroupHandler의 "부분 실패 경로" 원칙만으로 충분히 안전). base/dispatch-core-plan.md(정본, 다시 전면 재작성)/ base/module-lifecycle-plan.md/base/tag-plan.md/ base/attribute-plan.md/base/architecture.md 재반영, doc-check.py ERROR 0 유지.

교훈: 이번 라운드는 네 번 연속 정정이 있었음(op 위치 우선순위 → op 필수화 → 등록-선택 모델(틀림) → base가 스스로 등록(최종)) — 매번 "왜 이게 맞는지"를 서둘러 확정하기 전에 실제 아키텍처 전제(누가 Dispatch.addHandler를 부르는가)부터 정확히 확인했어야 했음. 이 전제 하나가 중간 두 라운드의 결론을 전부 무효화시켰음 — 확신 없는 아키텍처 주장은 재확인 없이 단정하지 말 것.

후속 — 세션 전체 자기 감사(사용자 요청 "다른곳에도 실수한거 있는지 찾아봐")

git diff로 이 세션에서 건드린 25개 파일 전체를 처음부터 다시 정독 — canBound/canExecute 재분리, Ref 0-W 해소, PreRef/PostRef/Observer/ Effect 동적 경로 가드, Tag/Attribute 등록 모델, 그리고 최초 코퍼스 감사 15건 전부 논리적 일관성 재확인. 실제 문제 3건 발견·수정:

  1. HUMAN_TODO.md가 0-W 해소 이후에도 "남은 건 0-W" 그대로 방치돼 있었음 — 세션 도중 question.md/CLAUDE.md는 0-W 해소를 반영했는데 HUMAN_TODO.md만 그 전 라운드(M4→M8 정정)에서 멈춰 있던 stale 갱신 누락. "결정 대기 절 자체가 비어있음"으로 정정.
  2. base/ref-plan.md의 "이중 배치 방지" 절에 편집 중 생긴 어색한 줄바꿈 (문장 중간에 orphan line) — 순수 포맷 정리.
  3. Tag/Attribute 등록 모델을 재정리하며 tag-plan.md/attribute-plan.md의 "백엔드가 통째로 다르게 처리하고 싶으면 override 가능" 문장을 실수로 빠뜨림 — "미지원 신호" 용도(opt-in FALLBACK+1 가로채기)만 남고 "알고리즘 전체 교체" 용도(그냥 더 높은 우선순위로 자기 Handler 등록)가 누락돼 있었음. 둘 다 유효한 별개 용도라 양쪽 파일에 복원.

나머지(canBound/canExecute 코드 블록의 순환·호출부 일관성, Ref bindLifetime/unbindLifetime 배치, FALLBACK 우선순위 값들, 세션 로그· CLAUDE.md 서술과 실제 base/ 반영 내용의 일치)는 전부 문제 없음 확인. doc-check.py ERROR 0 유지.

후속 — /code-review high가 잡은 3건 추가 발견·수정

사용자가 백그라운드로 /code-review high를 돌림 — 위 자기 감사에서 놓친 진짜 문제 3건을 잡아냄(전부 "canBound 폐기"라는 5차 세션 당시의 서술이 11차 세션 재도입 이후에도 안 고쳐진 곳들, 자기 감사가 git diff로 이 세션이 건드린 파일만 훑어서 안 건드린 파일 속 5차 세션 원본 서술은 놓쳤던 사각지대):

  1. audit/gcconn-trick-verification.md의 상단 배너(9-14행, 이 세션이 그 아래 섹션만 고치고 이 배너는 안 건드렸음)가 "canBound는 폐기됐고 게이트는 canExecute 하나"라고 여전히 서술 — 바로 몇 줄 아래(이번에 고친 섹션)와 정면 모순. 재정정.
  2. .claude/README.mdlifecycle-pattern.md/question-resolved.md/ canexecute-inst-arg-reversed.md/audit/ 인덱스 행 4곳 — 전부 이 세션 동안 한 번도 안 열어봤던 파일이라 5차 세션 서술("canBound 폐기")이 그대로 남아있었음. 전부 정정.
  3. luau-test/STATUS.md의 스파이크 10 재작성 가이드 — "이중 바인딩 게이트는 canBound가 아니라 canExecute로 쓸 것"이라고 옛 모델을 재작성 지침으로 명시하고 있었음(가장 심각 — 이대로 따라 재작성하면 또 틀린 스파이크가 나옴). 재정정.
  4. question.md의 "용어 정리" 절(canExecute 이름 논의)도 "canBound의 몫까지 canExecute가 겸함"이라는 5차 세션 전제 위에 서 있어서 같이 정정.

교훈: git diff만으로 하는 자기 감사는 "이 세션이 건드린 파일"의 내부 일관성만 보고, "이 세션이 안 건드렸지만 이 세션의 결정 때문에 stale해진 파일"은 놓침 — 이번처럼 이름 하나(canBound)가 재도입되면 그 이름을 인용하는 모든 파일을 grep으로 전수 스캔해야 하는데, 처음 자기 감사 땐 "내가 고친 파일들"만 재확인하고 "canBound"라는 문자열 자체로 전체 코퍼스를 훑지 않았음. doc-check.py ERROR 0 유지.