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

336 lines
22 KiB
Markdown

# 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.md`가 `base/`에서 같은
이유로 이동해온 것.
같은 처리를 `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` 둘이 됐다** — `sourceCountMap`
`valueEpochMap`, `sourceEmitMap``emitEpochMap`. 이름을 바꾼 이유는
"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)`. **다중 태깅**이 이 재작성의
존재 이유다(`Source`가 `SourceBrand`이면서 동시에 `EpochBrand`).
- **역조회 `Brand.get`은 없어졌다.**
- **유지된 것**: weak-key 레지스트리, 테이블 아이덴티티, duck-typing 기각
근거 둘, 포함 관계 predicate 합성, `Brand``None`에 의존하지 않는다는
결정, Luau narrowing 주의.
- **새로 명시한 것 하나** — 다중 태깅이 가능해져도 **포함 관계는 계속
predicate 합성으로 쓴다**(`Source`를 `StateBrand`에도 등록하지 않는다).
등록 지점이 흩어지면 조용한 버그가 되고 포함 관계가 코드에서 사라지기
때문. 정당한 다중 등록은 `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`
- **⚠️ 미해결이던 다중 의존성 중복 발화가 닫혔다** — `EffectHandle`
`EpochMap`을 하나 들고 각 내부 Observer가 받은 `from`으로 `Update`,
`true`일 때만 `fn`을 돌린다. `Effect`가 곧 deps의 공통 하류가 된다.
- **`Ref` 의존성은 이 맵에 안 낀다**(이 세션이 명시) — `Ref``Epoch`
아니고 `: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.md``reference/`는 또 **다른** 번호(§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.md``effect-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-test` → `archive/`+`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이 접힌다).
### 근거·표기 정확성
5. **`2^32` 랩이 "똑같이 도달 불가능"이라는 근거가 틀렸다.** 같은 척도(초당
100만 `Set`)로 `2^53`은 285년인데 **`2^32`는 약 72분**이다(20만 배 차이).
실제로 안전한 이유는 **도달 시간이 아니라 충돌 조건이 한 점**이라는 것
("정확히 `2^32`만큼 뒤처진 항목"이라야 하고, 한 바퀴 중 한 번이라도
건드려지면 갱신됨). 이 문서가 바로 위 항목에서 *"근거를 정확히 적을 것"*
이라 스스로 규정해놓고 20줄 뒤에 어긴 셈이라 그대로 정정했다.
6. `:Sync`를 "초기화에만 쓴다"고 적었으나 `gate-plan.md`가 flush 경로에서도
쓴다 → "반환값이 필요 없다고 이미 아는 곳 둘"로 정정.
7. `ROADMAP.md``Brand.luau` predicate 목록에 `isEpoch` 누락.
8. **역전된 옛 이름 잔재**`TweenTag`(3곳)와 `Effect(fn, state?)`(4곳).
후자는 code-review가 짚은 것보다 실제로 더 많았다
(`architecture.md`/`lifecycle-hooks-plan.md`/`ROADMAP.md`/`README.md`).
9. **절 재편에 안 따라온 `§` 참조 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`/`Inherit`은 `quad2-try``Base:Extends`
OOP 상속이 **확인된 죽은 접근**이라 그 어휘를 되살리면 오독을 부르고,
`Extract`는 코퍼스에서 이미 "소유권을 통째로 넘긴다"는 뜻이다.
**두 이름 다 `question.md`에 안 올린다** — 같은 자리에서 확정됐으므로 열린
항목이 아니다.