지금까지 세션 스크래치패드에만 있던 것을
.claude/audit/handtrace-round7-reference-impl/로 옮긴다. 다음 세션이
발견 52건을 판정하려면 이게 있어야 한다.
구성:
- spikes/core.luau — 반응형 코어(state-epoch §2~§5, 전파 모델,
gate 4·8번, Blocker, H-23 스냅샷)
- spikes/dispatch.luau — Length/Offset 부기(getOffsetAt 접두합 캐시,
recompute, setLength/setOffsetSource, 배치 게이팅)
- spikes/chain.luau — Dispatch 체인(하강 diff, retractFrom)
- spikes/*.luau — 시나리오 29개(런타임 18 + 타입 11)
- RUN-runtime.txt / RUN-typecheck.txt — 2026-08-25 실행 결과 스냅샷
- README.md — 원문 대조표 + 스파이크→발견 대조표
⚠️ README가 가장 중요하다: 이 참조 구현은 base/의 확정 의사코드를 손으로
옮긴 **전사물**이라 그 자체가 틀렸을 수 있다. 그래서 "재실행은 검증이
아니다"를 못박고, (a) 어느 파일이 어느 절을 옮긴 것인지 대조표와
(b) 문서와 **의도적으로** 다른 곳 3개(H-72 때문에 임시로 둔 PeekDiffers,
H-102의 A/B 대조를 위한 위치 박스, 생명주기 게이팅 부재=H-97)를 같이
싣는다. 그 셋 외의 모든 차이는 전사 오류 후보다.
부수로 round7 문서 머리에 이 폴더 포인터와 같은 경고를 달았고,
.claude/README.md의 audit/ 행에도 등재했다.
Co-authored-by: qwreey <me@qwreey.moe>
123 lines
5.6 KiB
Text
123 lines
5.6 KiB
Text
=== t1_diamond ===
|
|
초기 D:Get() = 110
|
|
Observer 발화 횟수: 1 | 관측값: 220
|
|
D 재계산 횟수: 2 | B: 2 | C: 2
|
|
2회차 후 발화: 2 | 관측값: 220,330
|
|
=== t2_error ===
|
|
-- A:Set(2) (전파 중 O_bad가 error) --
|
|
Set이 던졌는가: true | ./t2_error.luau:7: 사용자 콜백 실패
|
|
O_bad 발화: 1 | O_good 발화: 0 | 관측:
|
|
C의 값은 자가치유되는가: C:Get() = 2 (정상)
|
|
하지만 O_good은 이 변경에 대해 영원히 안 울었다: 0
|
|
-- 이후 A:Set(3)은 정상인가 --
|
|
두 번째 Set도 같은 자리에서 던짐: true | O_good 발화: 0
|
|
-- O_bad를 제거하고(구독 해제 없이 새 파동) --
|
|
O_good 발화: 1 | 관측: 4
|
|
→ 2와 3에 대한 통지는 영영 오지 않았다. 마지막 값만 보임
|
|
=== t3_gate_error ===
|
|
=== (a) blocker:Off() 순회 중 한 핸들이 error ===
|
|
보류 중 — g1.withheld 비었나: false | g2.withheld 비었나: false
|
|
Off()가 던졌는가: true | ./t3_gate_error.luau:11: g1 하류에서 실패
|
|
blocker.IsBlocked: false (이미 false — Off는 상태부터 바꾼다)
|
|
g2가 아직 붙들고 있나(비었으면 flush됨): false | o2 발화: 0
|
|
→ g2는 IsBlocked=false 인데도 밀린 통지를 못 내보냄. b:Off()를 다시 불러도:
|
|
두 번째 Off도 같은 자리에서 던짐: false | o2 발화: 1
|
|
|
|
=== (b) flush 도중 error → 떼어낸 배치가 소멸 ===
|
|
보류됨. withheld 비었나: false
|
|
flush 진입 후 withheld 비었나(스왑됨): true | flush 횟수: 1
|
|
gate.emitEpochMap은 이미 Sync됨 → 같은 리비전은 이제 규칙 3으로 삼켜진다
|
|
같은 리비전 재도착 후 o 발화: 1 | 관측:
|
|
→ 배치는 사라졌고 재도착도 삼켜져, 그 변경에 대한 통지가 영영 안 나감
|
|
(값은 살아있다: g:Get() = 2 )
|
|
=== t4_throttle ===
|
|
=== variant = localpending ===
|
|
타이머 살아있는 개수(idle이면 0): 0
|
|
통과: t=0.00 v=1 | t=1.00 v=2 | t=3.00 v=3
|
|
최종 통과: t=0.00 v=1 | t=1.00 v=2 | t=3.00 v=3
|
|
t=5까지 살아있는 타이머: 0
|
|
|
|
=== variant = nopending ===
|
|
타이머 살아있는 개수(idle이면 0): 1
|
|
통과: t=0.00 v=1 | t=1.00 v=2
|
|
최종 통과: t=0.00 v=1 | t=1.00 v=2 | t=4.00 v=3
|
|
t=5까지 살아있는 타이머: 1
|
|
=== t5_recompute_race ===
|
|
D:Get() = 2 (A=1로 계산됨)
|
|
A의 실제 값: 99
|
|
다시 D:Get() = 2 ← 기대값 198
|
|
D 재계산 횟수: 1
|
|
로그: fn이 읽은 A = 1
|
|
|
|
-- 이후 A를 또 바꾸면 회복되는가 --
|
|
D:Get() = 10 (회복됨)
|
|
=== t5b_fix ===
|
|
D:Get() = 2
|
|
다시 D:Get() = 198 ← 198이면 고쳐진 것
|
|
재계산 횟수: 2
|
|
=== t6_effect_gate ===
|
|
blocker ON — 이 구간의 변경은 gated 쪽에서 유보되어야 한다
|
|
fn 실행 기록: run(gated=2, plain=2)
|
|
gated가 아직 붙들고 있나: true
|
|
Off() 이후 fn 실행 기록: run(gated=2, plain=2)
|
|
gated observer 발화 횟수: 1 | plain observer: 1
|
|
=== t7_poisoned ===
|
|
-- 정상 배치 --
|
|
recompute 횟수: 1 | blocker.IsOn(): false
|
|
-- 두 번째 배치에서 사용자 코드가 error --
|
|
drive가 던졌는가: true | blocker.IsOn(): true ← 영구 On
|
|
-- 이후 런타임 :Add() 등이 부르는 setLength --
|
|
recompute가 몇 번 돌았나: 0 (0이면 그 owner는 영영 재계산 안 됨)
|
|
-- 새 배치를 열어도 --
|
|
blocker.IsOn(): false | 총 recompute: 2
|
|
=== t8_reentrant ===
|
|
관측 순서: 220,330
|
|
Observer 발화: 2 | D 재계산: 3
|
|
최종 D:Get() = 330 (A=3이므로 330이어야 함)
|
|
=== t9_gate_chain ===
|
|
[상류 먼저] 유보 중 g1=true g2=false | 발화 0
|
|
b1:Off 후 g1=false g2=true 발화=0
|
|
최종 발화 1 | v=5, batch=2
|
|
[하류 먼저] 유보 중 g1=true g2=false | 발화 0
|
|
b2:Off 후 g1=true g2=false 발화=0
|
|
최종 발화 1 | v=5, batch=2
|
|
=== t10_subscribe_gc ===
|
|
Observer 발화: 0 ← 공개 계약("예외 없이 그냥 계속 돎")대로면 1
|
|
A의 구독자 수: 0 ← 중간 State가 수거됐으면 0
|
|
Observer는 레지스트리에 살아있다: true | 그러나 다시는 안 울린다
|
|
=== d1_basic ===
|
|
offsets: 0 1 2 | 다음 자리: 3
|
|
recompute 횟수: 1
|
|
=== d2_slots ===
|
|
초기: s1.Offset = 0 | s2.Offset = 1 | recompute: 1
|
|
-- 런타임에 Slot1이 3개가 됨 (s1.Length:Set(3)) --
|
|
s1.Offset = 0 | s2.Offset = 4 (기대 4) | recompute: 2
|
|
-- Slot2도 2개가 됨 --
|
|
s2.Offset = 4 (기대 4) | 4번 자리 offset = 6 (기대 6)
|
|
-- Slot1이 다시 0개 --
|
|
s2.Offset = 1 (기대 1) | 4번 자리 = 3 (기대 3)
|
|
=== d4_reentrant ===
|
|
초기 owner.Length = 0 | s2.Offset = 0
|
|
-- s1.Length:Set(2) --
|
|
owner.Length = 2 | 실제 합계 = 3 | N = 3 | recompute: 3
|
|
s2.Offset = 2 (기대 2) | 3번 자리 offset = 2 (기대 2)
|
|
❌ owner.Length가 실제 합계와 다르다
|
|
=== d5_chain_error ===
|
|
-- 백엔드 미주입 상태에서 Attribute 한 번 --
|
|
던졌는가: true | ./d5_chain_error.luau:8: quad: setAttribute op이 주입되지 않았습니다(백엔드 팩토리 미설치)
|
|
claim 남았는가: true
|
|
체인 슬롯: 있음(retractor=function: 0x000055f521ddcb70)
|
|
-- 이제 백엔드를 붙이고 정상적으로 다시 시도 --
|
|
성공했는가: false | ./d5_chain_error.luau:16: attribute "Score" is already bound by another owner
|
|
-- 명시적 철거로 회수되는가 --
|
|
철거 후 claim 남았는가: true
|
|
철거 후 재시도 성공?: false | ./d5_chain_error.luau:16: attribute "Score" is already bound by another owner
|
|
=== d7_splice_fix ===
|
|
[확정 설계 그대로] s3.Offset = 1 (기대 5) | invalidAfter = 2
|
|
[위치 재조정함] s3.Offset = 5 (기대 5) | invalidAfter = 2
|
|
=== d9_hole2 ===
|
|
setOffsetSource에서 터지는가: true
|
|
메시지: ./dispatch.luau:46: attempt to perform arithmetic (add) on number and nil
|
|
=== e1_errlevel ===
|
|
level 기본(1): ./e1_errlevel.luau:5: Slot:Add — index가 범위 밖(1..3): 9
|
|
level 2 : ./e1_errlevel.luau:12: Slot:Add — index가 범위 밖(1..3): 9
|