quad/.claude/session/2026-08-21-03-epoch-brand-promotion-and-flatten.md
qwreey 0498816c10
design: Epoch/EpochMap/Brand 전면 승격 + 해소 기록 flatten
앞 세션이 컨텍스트 피로로 미뤄둔 승격(todos 000번)을 수행하고, 코퍼스에
쌓여 있던 [해소]/[정정] 층을 걷어냈다. 감사 3라운드 + /code-review high로
15건을 잡아 전부 반영했다.

승격 — base/ 넷 + 파급 넷
- state-epoch-plan.md 재작성: Epoch 인터페이스({Revision:number}, 그 자체로
  키가 되는 unique 테이블, Source가 구조적으로 만족)와 EpochMap(Update/
  Refresh/Sync/TrackFrom) 신설. State는 EpochMap을 둘 컴포지션 —
  sourceCountMap/sourceEmitMap -> valueEpochMap/emitEpochMap. §1~§8로 재편.
- brand-plan.md 전면 재작성: 공유 레지스트리 + Brand.get(객체당 태그 하나)
  -> 인스턴스 브랜드 Brand() + :register/:is, 다중 태깅 허용. 발단은 Source가
  SourceBrand이면서 동시에 EpochBrand여야 하는데 옛 모양으로 표현 불가.
  역조회는 제거(전수 조사에서 쓰는 자리 0). weak-key/테이블 아이덴티티/
  duck-typing 기각 근거/predicate 합성은 전부 유지.
- source-state-plan.md: Source가 Epoch도 구조적으로 만족(Revision은 공개여야
  타입 레벨에서 성립), Observer 클로저가 fn(self, from: (Epoch|EpochSet)?).
-  effect-plan.md: 다중 의존성 중복 발화 미해결 항목이 닫힘 —
  EffectHandle이 자기 EpochMap을 들어 첫 번째만 통과시킨다.
- gate-plan.md/architecture.md/bind-system-plan.md/ROADMAP.md 어휘 통일,
  EpochMap.luau가 M2에도 필요하다는 것 반영(GateNode가 씀).

리비전 갱신은 bit32.bnot(-rev) — 사용자 확정
- a>0이면 a-1, 0이면 4294967295인 랩어라운드 감소. 갱신과 랩이 FASTCALL
  하나로 끝난다(luau 실측). 근거: 2^53 포화는 도달 불가능한데 그걸 피하려고
  값을 double 영역까지 키울 이유가 없다, 매번 도는 hot path다.
- 에이전트가 이걸 band(rev+1, mask)로 잘못 옮기고 "그러니 bit32가 더 싼 건
  아니다"라는 틀린 단서까지 달았다가 사용자 정정("제가 말한건 bit32.bnot(-a)
  입니다"). 세 문서에 정정 경위를 남겼다.
- 따름정리: 리비전은 증가가 아니라 감소한다. ==/~= 만 쓰는 지금 규칙에서만
  무해하다는 경고를 §2에 명시.

archive / flatten
- archive/brand-shared-registry-reversed.md 신설(옛 Brand 표면 원문).
- question.md 421->208줄: 해소 항목 18건을 archive/question-resolved.md로
  이관. 그 문서가 스스로 정한 규칙("해소하면 여기서 지우고 archive로")을
  다시 어기고 있었다. 이관분의 옛 필드명은 소급 수정하지 않고 머리에 경고만.
- todos.md 000번 삭제, "M3 착수 전 필요" 목록에서 해소 항목 일곱 제거
  (실제로 열린 건 중간 State GC와 store:GetDynamic 둘뿐).
- research/ -> reference/ 이동 둘(epoch-brand-composition,
  slot-attach-decomposition). 확정된 결정의 근거 기록은 research(상의 필요)도
  archive(뒤집힘)도 아니므로, reference의 폴더 기준에 그 용도를 명문화했다.

감사 3라운드(각도: base 정합성 / 인덱스+luau-test / archive+qa-request)
- §8에 "기각된 대안 — 게이트를 에포크 경계로" 논거 신설(재작성 때 떨어뜨렸고,
  두 문서가 서로 다른 없는 §번호를 대고 있었다).
- luau-test 스파이크 22를 done/ -> rewrite-required/(옛 Brand.set/get을 직접
  구현). STATUS.md 개수와 절 제목의 하드코딩 개수 정리.
- state-epoch/source-state가 "Revision을 증가시킨다"고 적어놓고 20줄 뒤에
  "감소한다"로 반박하던 자기모순 정정.

커밋 전 /code-review high — 9건, 전부 유효(감사자가 못 보는 축)
-  {Epoch}는 Luau에서 배열인데 실제 게이트 배치는 {[Epoch]:true} 집합.
  그대로 ipairs로 구현하면 유보됐다 풀린 emit이 전부 삼켜진다(gate-plan 4번이
  애초에 고치려던 그 버그) -> EpochSet으로 확정.
-  새 노드 시딩이 확정된 EpochMap 표면으로 표현 불가능했다(:With의 상류는
  State이지 Epoch가 아니고, 키 열거/병합 연산이 없었음) -> :TrackFrom 신설.
- GateNode 예외(emitEpochMap을 전파 시점에 갱신)가 §4에 미기록.
- 설치 발화엔 from이 없다 -> 옵셔널로, Effect의 억제 플래그가 Update보다
  먼저여야 함을 명시.
- 2^32 랩을 "똑같이 도달 불가능"이라 한 근거가 틀렸다(같은 척도로 285년 vs
  72분). 실제 안전 근거는 충돌 조건이 한 점이라는 것으로 정정.
- 그 외 :Sync 용도 충돌, isEpoch 누락, TweenTag 3곳, Effect(fn,state?) 4곳,
  § 참조 3곳.

에이전트가 이름 붙인 연산 둘은 사용자 검토로 확정
- :Refresh 유지 — "Update는 받은 것을 처리, Refresh는 내가 받았던 걸 처리라
  표면적 의미 자체가 다르다"(오버로드로 합치지 않음).
- :Absorb -> :TrackFrom 개명 — absorb는 상위에서 제거할 것처럼 읽히고,
  gate-plan이 이미 "흡수 집합"을 다른 뜻으로 쓴다. TrackFrom은 이 맵의 존재
  이유("내가 뭘 추적하고 있나")를 그대로 쓰고 From이 비파괴를 못박는다.

Epoch/EpochMap/Brand에 열린 설계 항목 없음. doc-check.py ERROR 0.

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01TiW21rnti9SbLgF6twtn6D
2026-08-22 01:27:18 +09:00

22 KiB

2026-08-21-03 — Epoch/EpochMap/Brand 전면 승격 + 해소 기록 flatten

요지: 앞 세션(2026-08-21-02-qa-round5-and-gate-epoch-research.md)이 컨텍스트 피로로 미뤄둔 승격을 실제로 수행하고, 그 과정에서 코퍼스 곳곳에 쌓여 있던 [해소]/[정정] 층을 걷어냈다. 지금 유효한 설계는 전부 base/가 소스 — 이 파일은 "무엇을 어디로 옮겼나"의 기록이다.

1. 사용자 지시

두 갈래였다.

  1. 질문: "slot-attach-decomposition.md 은 이제 확정 이야기이기에, 리서치 대상이 아니지 않나요? 또, 이것이 base에 적용되어 있습니까?"
  2. 작업 지시: state-epoch-plan.md/source-state-plan.md/effect-plan.md/ brand-plan.md 넷을 전면 승격하고, "이 승격을 하며 더이상 필요 없어진 요소들은 archive 하세요. 특히 재정정 기록이 쌓인 부분이 있는데, 풀어가며 archive 해서 flatten 해야할 필요가 보입니다."

2. 1번 질문에 대한 답 — 둘 다 예

  • base/slot-plan.md에 이미 반영돼 있다materializeSlotTree / mountSlotTree / 두 줄짜리 attachSlot 래퍼가 그 문서의 "재귀 메커니즘" 절에 의사코드로 들어가 있고, 그 문서가 근거로 이 파일을 네 곳에서 인용한다.
  • research/에 있을 이유는 없다.claude/README.md의 폴더 기준상 research/는 "아직 착수 전, 상의 필요"다.

어디로 옮길지가 실제 판단이었다. archive/는 기준이 "뒤집혔거나 기각됨" 이라 채택된 결정의 근거 기록엔 안 맞고, reference/의 기준("결정 자체가 아니라 다른 문서가 근거로 인용하는 온디맨드 자료")엔 정확히 맞는다. 그래서 reference/로 옮기고 폴더 기준 자체에 그 용도를 명문화했다 (README.md 폴더 기준표 + project-context.md). 선례도 있다 — quad-v1-architecture.md/comparison-fusion-vide.mdbase/에서 같은 이유로 이동해온 것.

같은 처리를 epoch-brand-composition.md에도 적용했다(승격이 끝나면 성격이 똑같아지므로).

3. 승격 — 무엇이 어떻게 바뀌었나

base/state-epoch-plan.md (재작성)

  • Epoch 인터페이스 신설type Epoch = { Revision: number }, 그 자체로 키가 되는 unique 테이블. Source가 구조적으로 만족하고 EpochBrand에 등록된다. "루트 Source의 에포크"라는 좁은 서술이 전부 여기로 일반화됨.
  • EpochMap 신설:Update(Epoch|{Epoch}) -> boolean(반환값의 뜻은 "뒤로 전파가 필요한가"), :Refresh(), :Sync(). 부기가 State에서 떨어져 나와 재사용 가능한 객체가 됐다.
    • :Refresh는 이 세션이 이름만 붙인 것이다 — "순회"라는 연산 자체는 이미 확정돼 있었고(rawInvalid == false일 때 자기 맵을 훑는다), 맵이 키를 소유하므로 인자 없는 형태가 자연스러워서 그렇게 적었다. 새 설계가 아니다.
  • State의 두 맵이 EpochMap 둘이 됐다sourceCountMapvalueEpochMap, sourceEmitMapemitEpochMap. 이름을 바꾼 이유는 "source"가 더 이상 정확하지 않아서다.
  • 문서 구조를 §1~§8로 다시 짰다. 옛 §3(에이전트 분석)과 §5(열린 질문)에 결론과 정정 경위가 섞여 있었는데, 결론은 규칙 본문(§2~§5)으로 올리고 경위는 걷어냈다(원문은 qa-request/pre-implementation-qa-round5-followup.md M·N절과 reference/epoch-brand-composition.md에 있다).

base/brand-plan.md (전면 재작성)

  • 인스턴스 브랜드Brand()가 브랜드마다 weak 집합 하나를 들고, SomeBrand:register(x) / SomeBrand:is(x). 다중 태깅이 이 재작성의 존재 이유다(SourceSourceBrand이면서 동시에 EpochBrand).
  • 역조회 Brand.get은 없어졌다.
  • 유지된 것: weak-key 레지스트리, 테이블 아이덴티티, duck-typing 기각 근거 둘, 포함 관계 predicate 합성, BrandNone에 의존하지 않는다는 결정, Luau narrowing 주의.
  • 새로 명시한 것 하나 — 다중 태깅이 가능해져도 포함 관계는 계속 predicate 합성으로 쓴다(SourceStateBrand에도 등록하지 않는다). 등록 지점이 흩어지면 조용한 버그가 되고 포함 관계가 코드에서 사라지기 때문. 정당한 다중 등록은 Epoch처럼 다른 축의 계약뿐이다.
  • 옛 표면은 archive/brand-shared-registry-reversed.md.

base/source-state-plan.md

  • "Source가 State를 만족함" 절에 Epoch도 같은 방식으로 만족한다는 항목 추가(Revision이 공개여야 하는 이유 포함).
  • state:Observer(fn) 절에 fn(self, from: Epoch | {Epoch}) 시그니처 확정 추가. "값을 안 실어주는 구독" 계약이 안 깨지는 이유도 같이.
  • 승격 대기 ⚠️ 배너 제거, 그 절들의 "에포크" 표현을 "리비전"으로 통일.

base/effect-plan.md

  • ⚠️ 미해결이던 다중 의존성 중복 발화가 닫혔다EffectHandleEpochMap을 하나 들고 각 내부 Observer가 받은 from으로 Update, true일 때만 fn을 돌린다. Effect가 곧 deps의 공통 하류가 된다.
  • Ref 의존성은 이 맵에 안 낀다(이 세션이 명시) — RefEpoch가 아니고 :Callback으로 발화해 from이 없다. 공통 상류 문제 자체가 없다.

base/gate-plan.md

승격 대기 배너를 반영 완료로 바꾸고, 페이로드/withheld/emitEpochMap 표현을 Epoch 어휘로 통일. 기제는 하나도 안 바뀌었다.

4. flatten — 걷어낸 것들

사용자가 지목한 "재정정 기록이 쌓인 부분"은 실제로 세 군데였다.

  1. question.md가 다시 해소 항목으로 절반이 찼다. 그 문서 스스로 *"항목을 해소하면 여기서 지우고 archive/question-resolved.md에 근거와 함께 옮길 것 — 다시 쌓이면 같은 문제가 반복됨"*이라 규정해뒀는데, 2026-08-21 하루에 여러 건이 닫히면서 예고대로 재발했다. 16건을 일괄 이관하고(Gate 이름 항목 포함), 파일이 421줄 → 208줄이 됐다.
    • 이관분 중 Epoch 관련 서술은 이관 시점 표현 그대로 뒀고, 대신 이관 섹션 머리에 "필드 이름이 그 뒤 바뀌었다"는 경고를 붙였다. 히스토리 문서의 원문을 소급해 고치지 않는다는 관례를 지키면서 오독만 막는 처리.
  2. todos.md의 "M3 착수 전에 결론이 필요한 항목 목록"이 절반 넘게 [해소] 였다. 실제로 열려 있는 건 둘뿐(중간 State GC 미검증, store:GetDynamic) 인데 해소 항목 일곱이 섞여 있어 "지금 할 일"로 안 읽혔다. 걷어냄.
  3. todos.md 000번(다음 세션 첫 작업 = 이 승격)은 완료됐으므로 삭제하고, 00번 안에 승격 완료 사실만 남겼다.

5. 마지막 미정도 같은 세션에 닫힘 — 리비전 증가는 bit32

승격 보고 직후 사용자가 그 자리에서 정했다: "리비전 증가는 bit32 로 두고싶어요. 그건 luau 에서 native call 이라 아주 빨라요. 반면 double 의 연산이 느린편인데, 희소 수준이 아니라, 사실상 만나는걸 수년간 보기 어려운 라운드되어 동일해 무시되는 경우를 막기 위해 double 까지 올려야할 이유를 모르겠어요. 매번 도는 코드인지라, 값 싸게 native call + num 연산으로 가볍게 가고 싶어요."

논거의 핵심은 **"2^53 포화는 어차피 도달 불가능한 시나리오인데, 그걸 피하겠다고 값을 double 영역까지 키울 이유가 없다"**이고, 매 Set마다 도는 hot path라는 것.

⚠️ 에이전트가 형태를 잘못 옮겼고, 사용자가 그 자리에서 정정했다. 에이전트는 이걸 bit32.band(rev + 1, 0xFFFFFFFF)로 적고, 거기에 "그러니 bit32라서 증가가 더 싼 건 아니다 — n + 1은 어느 쪽이든 double 덧셈이고 bit32는 fastcall을 하나 더 얹는다"는 단서까지 달았다. 사용자 정정:

"잠시만요. 제가 말한건, bit32.bnot(-a) 입니다" — 그리고 REPL 출력 셋을 그대로 제시했다(bit32.bnot(-1)0, bit32.bnot(-0)4294967295, bit32.bnot(-4294967295)4294967294).

즉 그 세 줄은 예시가 아니라 연산 자체였다. luau로 재확인한 결과:

bump(a) = bit32.bnot(-a)
0 → 4294967295 | 1 → 0 | 2 → 1 | 4294967295 → 4294967294

a > 0이면 a - 1, 0이면 4294967295랩어라운드 감소이고, 갱신과 랩이 FASTCALL 하나로 끝난다. 그래서:

  • 에이전트가 붙였던 단서는 틀렸다band(rev + 1, mask)라는 다른 형태를 놓고 한 비교였다. bnot(-a)는 덧셈 위에 얹히는 게 아니라 갱신 자체를 대체하므로, *"native call 이라 아주 빨라요"*라는 사용자 서술이 맞다.
  • 따름정리는 방향만 바뀌어 유효하다 — 단조 증가가 아닌 게 아니라 아예 감소한다. 지금 규칙이 ==/~=만 쓰기 때문에 무해하지만, 순서 비교를 넣고 싶어지면 이 결정부터 되짚어야 한다. Revision이라는 이름이 순서를 뜻하지 않는다는 것도 같이 적어뒀다.

교훈: 사용자가 근거로 든 REPL 출력을 "그 함수가 랩한다는 예시"로만 읽고 연산 형태를 에이전트가 임의로 재구성했다. conventions.md의 "사용자 발언을 근거로 인용할 때는 결론만 적지 말고 논거까지 남길 것" 항목이 겨냥하는 실패에 가깝다 — 이 경우엔 논거를 남기긴 했는데 형태를 바꿔 남겼다.

그래서 Epoch/EpochMap/Brand에 열린 설계 항목은 하나도 남지 않았다.

doc-check.py ERROR 0 유지(WARN 30건은 전부 이 세션 이전부터 있던 것 — 축약 파일명 인용과 날짜 없는 시한부 주장).

6. 감사 루프 기록

conventions.md의 "핸드오버 준비하고 커밋해" 절차대로 한 턴에 하나씩, 라운드마다 각도를 바꿔 돌렸다.

1라운드 — base/ 정합성 + diff 범위

발견 넷 중 하나는 stale(감사자가 bit32 편집 이전 스냅샷을 읽어 "리비전 증가 방식이 아직 미정"이라 보고했으나 다섯 곳 전부 이미 갱신돼 있었음 — 감사자가 도는 도중 메인 세션이 같은 파일을 고치면 생기는 일이라, 감사 리포트를 받으면 항상 현재 파일로 재확인할 것). 나머지 셋은 유효:

  1. state-epoch-plan.md가 "§8에서 기각됨"으로 실재하지 않는 내용을 가리켰다 — 재작성하면서 "게이트를 에포크 경계로" 기각 논거를 떨어뜨렸고, gate-plan.mdreference/는 또 다른 번호(§5-3)를 대고 있었다. → §8에 실제 논거를 쓰고 세 포인터를 절 제목으로 통일. doc-check.py는 산문 속 §번호를 검증하지 못한다 — 문서를 재작성할 때 §번호 포인터는 손으로 훑어야 한다.
  2. ROADMAP.md M7이 isState/isSource를 아직 "공유 레지스트리 기반"으로 서술 → 멤버십 기반으로.
  3. 스파이크 22가 역전된 Brand.set/Brand.get을 직접 구현한 채 done/에 있었다rewrite-required/로 이동. 검증 대상(포함 관계)은 유효하므로 결론이 틀려서가 아니라 구현자가 그 파일의 Brand 구현을 참고 모델로 오독하는 걸 막기 위함. 05와 같은 처리.

2라운드 — 인덱스 레이어 + luau-test + 1라운드 이후 diff

여섯 건, 전부 유효:

  1. state-epoch-plan.md가 자기 문서 안에서 모순했다 — §2가 "Revision증가시킨다"라고 적어놓고 20줄 뒤에 "확정된 방식은 증가가 아니라 감소한다"라고 반박. bit32.bnot(-a) 정정을 반영하면서 앞쪽 문장을 안 고친 것. → 방향을 안 담는 "갱신한다"로.
  2. source-state-plan.md같은 문장이 복붙돼 있었다 — 같이 정정.
  3. README.mdeffect-plan.md 행이 dedup 대칭을 아직 "미확인"으로 서술 (본문은 EF-3에서 이미 "성립함"으로 확정). 세션 전부터 있던 stale.
  4. STATUS.md✅ done/ 절이 같은 파일 위쪽과 어긋났다 — 배너와 표는 22 이동을 반영했는데 절 제목("17건")과 본문 나열("런타임 9건 …/22")은 옛 상태. → 하드코딩 개수를 빼고 폴더/표를 소스로.
  5. luau-test/README.md의 "실행 환경 세 갈래" 표가 21/22/23을 빠뜨리고 13을 아직 "런타임/타입 부분"으로 쪼개 적고 있었다(런타임 절반은 2026-08-19에 22로 분리돼 나갔다). → 실제와 맞추고, 파일 헤더가 소스임을 명시.
  6. reference/slot-attach-decomposition.md의 제약 C7이 그 뒤 native* 계층 확정으로 일반 계약으로서는 폐기됐는데 문서에 아무 표시가 없었다. 결론 자체는 유효하지만(배치 경로가 부기를 먼저 끝내는 건 C6 요구사항이라 별개) 표만 떼어 읽으면 오독하므로 상단에 캐비엇 배너. reference/comparison-fusion-vide.md가 이미 같은 형태의 캐비엇을 달고 있어 선례를 따랐다.

교훈 하나 — 4·5번은 둘 다 "같은 문서 안에서 위쪽만 갱신되고 아래쪽이 남은" 형태다. 개수를 제목에 박아두면 정확히 이렇게 갈라지므로, 이번에 STATUS.md의 절 제목 두 곳에서 하드코딩 개수를 뺐다.

3라운드 — archive/ 배너 정합성 + qa-request/의 "반영 완료" 주장

확실한 모순 발견 0건 — 수렴. 감사자가 대조한 것: archive/의 배너 포인터 넷(brand-shared-registry-reversed / question-resolved의 이관 섹션 / always-propagate-no-dedup-superseded / invalidate-dedup-propagation-reversed / bookkeeping-before-physical-reversed), 2라운드가 고친 여섯 자리, 그리고 round4/5-followup.md가 소스라고 선언한 항목들.

특히 2라운드에서 내가 새로 쓴 C7 캐비엇("배치 경로가 부기를 먼저 끝내는 건 C7이 아니라 C6가 요구하는 별개 사안")이 base/dispatch-core-plan.md의 "일반 계약 — 물리와" 절 4번과 정확히 대응함이 확인됐다 — 새로 쓴 서술이 기존 확정과 어긋나지 않는지가 이 라운드의 핵심 질문이었다.

유일한 지적은 문체였다 — §8에 "Revision올리면"이라는 방향 어감이 남아 있던 것(§2가 이미 "방향은 계약이 아니다"로 커버하고 있어 모순은 아님). 같이 정리했다.

수렴 판정: 3라운드에서 새 발견 0건이므로 conventions.md의 루프 종료 조건을 만족한다. 총 소비는 서브에이전트 3패스(각도: base/ 정합성 → 인덱스 레이어+luau-testarchive/+qa-request/).

7. 커밋 전 /code-review high — 감사 3라운드가 못 본 축에서 9건

conventions.md가 명문화한 그대로였다 — 감사자와 code-review는 보는 축이 다르다. 감사자 3라운드가 수렴(0건)한 뒤 사용자가 /code-review high를 돌리자 9건이 더 나왔고 전부 유효했다. 감사자는 코퍼스 전체의 의미론적 정합성(A 문서 결정 ↔ B 문서 서술)을 보고, code-review는 diff 자체의 결함(이번에 새로 쓴 서술 안의 모순, 새 표면이 기존 계약과 충돌하는가)을 본다.

구현을 실제로 막았을 것들

  1. {Epoch} 표기가 실제 배치 모양과 달랐다. Luau에서 {Epoch}배열({[number]: Epoch})인데, 실제로 넘어오는 게이트 배치는 gate-plan.md 4번이 확정한 withheld : { [epoch] : true }집합이다. 이 표기를 믿고 ipairs로 구현하면 배치 순회에서 원소가 0개가 되어 유보됐다 풀린 emit이 전부 조용히 삼켜진다. gate-plan.md 4번이 애초에 고치려던 바로 그 버그였다. → type EpochSet = { [Epoch]: true }로 확정. 집합이어야 하는 이유는 게이트 쪽 요구다(흡수·unfold가 저절로 접혀야 함). 이건 사용자 원 제안의 Epoch|{Epoch} 표기를 에이전트가 그대로 옮기면서 Luau 타입으로서 뭘 뜻하는지 확인 안 한 결과다.
  2. 새 노드 시딩이 확정된 EpochMap 표면으로 표현 불가능했다. "상류의 Epoch를 전부 끌어와 채운다"인데 :With(a, b)의 상류는 State이지 Epoch가 아니다. 상류가 추적 중인 루트 집합은 그 State의 valueEpochMap 안에만 있는데 §3 표면엔 키 열거도 병합도 없었다.:TrackFrom(other) 신설(가칭 Absorb, 같은 날 개명 — §8 참고). :Refresh와 같은 성격으로 이미 확정된 동작에 이름을 붙인 것이지 새 설계가 아니다(사용자 확정 문구가 이미 *"전부 가져와서, 실제 count 로 둡니다"*였다). dep이 Epoch:Sync, State면 :TrackFrom으로 갈리는 것도 같이 적었다.
  3. GateNode 예외가 §4에 기록돼 있지 않았다. §4 의사코드는 emitEpochMap:Update(from)수신 시점에 무조건 부르는데, gate-plan.md 4번은 게이트가 전파할 때 :Sync(batch)로 갱신하는 것으로 확정돼 있다. §4대로 구현하면 그 계약이 조용히 깨진다.
  4. 설치 시 즉시 1회 발화에 from이 없다. fn(self, from)을 non-optional로 선언해뒀는데 등록 시점 발화엔 출처가 없다 — Effect의 확정 클로저가 Update(nil)을 부르게 된다. → 옵셔널로 바꾸고, Effect의 억제 플래그가 Update보다 먼저 와야 함을 명시(순서를 뒤집으면 설치 발화가 맵을 건드려 그 파동의 첫 진짜 emit이 접힌다).

근거·표기 정확성

  1. 2^32 랩이 "똑같이 도달 불가능"이라는 근거가 틀렸다. 같은 척도(초당 100만 Set)로 2^53은 285년인데 2^32는 약 72분이다(20만 배 차이). 실제로 안전한 이유는 도달 시간이 아니라 충돌 조건이 한 점이라는 것 ("정확히 2^32만큼 뒤처진 항목"이라야 하고, 한 바퀴 중 한 번이라도 건드려지면 갱신됨). 이 문서가 바로 위 항목에서 "근거를 정확히 적을 것" 이라 스스로 규정해놓고 20줄 뒤에 어긴 셈이라 그대로 정정했다.
  2. :Sync를 "초기화에만 쓴다"고 적었으나 gate-plan.md가 flush 경로에서도 쓴다 → "반환값이 필요 없다고 이미 아는 곳 둘"로 정정.
  3. ROADMAP.mdBrand.luau predicate 목록에 isEpoch 누락.
  4. 역전된 옛 이름 잔재TweenTag(3곳)와 Effect(fn, state?)(4곳). 후자는 code-review가 짚은 것보다 실제로 더 많았다 (architecture.md/lifecycle-hooks-plan.md/ROADMAP.md/README.md).
  5. 절 재편에 안 따라온 § 참조 3곳 — 1라운드가 같은 유형을 잡았는데도 남아 있었다. doc-check.py§번호를 검증 못 하는 사각지대다.

교훈: 1·2번은 둘 다 "사용자 제안의 표기를 그대로 옮겼는데 그게 실제 메커니즘과 안 맞는" 유형이다. 승격은 문장을 옮기는 작업이 아니라 표기가 가리키는 것이 실제로 성립하는지 확인하는 작업이라는 게 이번의 교훈.

8. :Refresh/:TrackFrom — 에이전트가 이름 붙인 둘, 사용자 검토로 확정

/code-review까지 처리한 뒤, 에이전트가 임의로 이름 붙인 연산 둘을 사용자에게 명시적으로 올렸다. 동작 자체는 회신에 이미 확정돼 있었지만 표면 이름은 에이전트 판단이었기 때문 — conventions.md의 "애매하면 임의로 정하지 말고 그 자리에서 사용자에게 보고할 것"에 해당한다.

:Refresh — 그대로 확정. 사용자가 :Update와 합치지 않는 이유를 직접 정리했다: "Update 에 인자 없는건 좀 아니야. 뭔가 받아서 받은것들에 대해서 처리하겠다는건데, 리프레시는 아무래도 내가 받았던걸 처리하겠다는거라 표면적 의미 자체가 다르지."인자를 받아 그것을 처리하는 연산자기가 이미 들고 있는 것을 처리하는 연산은 표면적 의미가 달라서 오버로드로 합치면 안 된다는 것. 표면이 하나 줄어드는 것보다 이 구분이 값이 크다.

:Absorb:TrackFrom으로 개명. 사용자 지적: "absorb 는 조금 상위 요소꺼를 흡수해서 상위 요소에서 제거할것만 같은 이름이긴 하네." 맞다 — 이 연산은 other전혀 안 건드린다. 덧붙여 base/gate-plan.md가 이미 "흡수 집합"을 다른 뜻(emit을 붙들고 있음)으로 쓰고 있어, 한 코퍼스 안에 같은 단어가 두 의미로 놓이는 문제도 있었다.

후보를 넷 올렸고(TrackFrom/SeedFrom/Include/Adopt) 권장안이 채택됐다. TrackFrom을 고른 근거 셋:

  1. 이 맵의 존재 이유를 사용자가 표현한 말이 "추적"이었다"'내가 뭘 추적하고 있나' 가 필요하죠"(§4의 시딩 규칙 근거). 코퍼스가 이 맵을 설명하는 말과 메소드 이름이 일치한다.
  2. From이 방향을 못박아 비파괴가 드러난다Absorb가 실패한 지점.
  3. SeedFrom보다 오래 간다 — 동적 의존성으로 "생성 이후에 키를 더하는" 자리가 생겨도 이름이 그대로 맞다. Seed는 그때 거짓말이 된다.

배제한 것도 근거가 있다 — Extend/Inheritquad2-tryBase:Extends OOP 상속이 확인된 죽은 접근이라 그 어휘를 되살리면 오독을 부르고, Extract는 코퍼스에서 이미 "소유권을 통째로 넘긴다"는 뜻이다.

두 이름 다 question.md에 안 올린다 — 같은 자리에서 확정됐으므로 열린 항목이 아니다.