--[[ Store 계약 — `.claude/base/store-plan.md` "Store = Source들의 이름 붙은 모음 (명시적 초기화)" / "타입 추론 문제 — `store.key`(dot-access)를 1급 경로로 확정" / "`store.key` 레코드 필드 타이핑". `H-122`(isSource 화이트리스트) / `H-153`(예약 이름 런타임 가드, 그림자 = store 자신) / `H-83`(무인자). ]] local Quad = require("../src") local QuadTypes = require("../roblox_packages/quad_types") type Source = QuadTypes.Source type State = QuadTypes.State local Source, Store = Quad.Source, Quad.Store print("=== 1. 명시적 초기화 — store.key는 넣은 Source 그 자체, 평범한 레코드 필드 ===") do local hp, name = Source(100), Source("x") local s = Store({ hp = hp, name = name }) assert(s.hp == hp and s.name == name, "fields are the very Sources passed in") assert(rawget(s, "hp") == hp, "shadow = the store itself (H-153)") assert(Quad.isStore(s) and not Quad.isSource(s), "brand") local v: number = s.hp:Get() assert(v == 100, "typed dot-access") s.hp:Set(5) assert(hp:Get() == 5, "Set goes to the same Source") -- 무주석 콜백 파라미터 추론이 사는 선언 스타일(§1②) — analyze가 지킨다 local doubled: State = s.hp:Compute(function(x) return x:Get() * 2 end) assert(doubled:Get() == 10, "Compute through a store field") print("PASS") end print() print("=== 2. defaults 검증 (H-122) — isSource 화이트리스트, level 2, 영어 ===") do local ok, err = pcall(function() Store({ hp = 100 } :: any) end) assert(not ok and string.find(tostring(err), "not a Source", 1, true) ~= nil, "raw value rejected: " .. tostring(err)) assert(string.find(tostring(err), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller") local ok2 = pcall(function() Store({ ok = Source(1), bad = Quad.Ref(1) } :: any) end) assert(not ok2, "a Ref is not a Source") -- H-204: the bag itself is validated before clone — a non-table used to die -- inside table.clone, a bare Source got an error naming a Source-internal field. local ok3, err3 = pcall(function() Store(5 :: any) end) assert(not ok3 and string.find(tostring(err3), "must be a table", 1, true) ~= nil, "non-table defaults: " .. tostring(err3)) assert(string.find(tostring(err3), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller") local ok4, err4 = pcall(function() Store(Source(100) :: any) end) assert(not ok4 and string.find(tostring(err4), "plain table", 1, true) ~= nil, "bare Source as defaults: " .. tostring(err4)) local ok5 = pcall(function() Store(setmetatable({}, { __metatable = "locked" }) :: any) end) assert(not ok5, "protected metatable rejected before clone") print("PASS") end print() print("=== 3. 예약 이름 (H-153) — 생성자 순회와 Of 둘 다 error(level 2) ===") do for _, name in { "Of", "Names", "__reservedCheck" } do local ok, err = pcall(function() Store({ [name] = Source(1) } :: any) end) assert(not ok and string.find(tostring(err), "reserved", 1, true) ~= nil, name .. " in defaults: " .. tostring(err)) local s = Store({} :: {}) local ok2, err2 = pcall(function() s:Of(name) end) assert(not ok2 and string.find(tostring(err2), "reserved", 1, true) ~= nil, name .. " via Of: " .. tostring(err2)) assert(string.find(tostring(err2), "spec.store.luau", 1, true) ~= nil, "level 2") end -- H-201: Of exists for computed names, so a computed nil/number must error -- here (nil used to allocate a Source then die at an internal frame; a number -- silently broke `Names(): { string }`). local s = Store({} :: {}) local okNil, errNil = pcall(function() s:Of(nil :: any) end) assert(not okNil and string.find(tostring(errNil), "Of name must be a string", 1, true) ~= nil, "nil Of name: " .. tostring(errNil)) assert(string.find(tostring(errNil), "spec.store.luau", 1, true) ~= nil, "level 2 points at the caller") local okNum, errNum = pcall(function() s:Of(123 :: any) end) assert(not okNum and string.find(tostring(errNum), "Of name must be a string", 1, true) ~= nil, "number Of name: " .. tostring(errNum)) assert(#s:Names() == 0, "nothing was allocated by the rejected names") print("PASS") end print() print("=== 4. :Of — 없는 이름은 그 자리에서 만들어 저장(lazy가 남는 유일한 자리), 있으면 그것 ===") do local s = Store({ hp = Source(1) }) local dyn: Source = s:Of<>("dyn") assert(Quad.isSource(dyn) and dyn:Get() == nil, "created as Source(nil)") assert(s:Of("dyn") == dyn, "same name → same Source") assert(rawget(s, "dyn") == dyn, "stored on the store itself") assert(s:Of("hp") == s.hp, "declared keys are reachable through Of too") print("PASS") end print() print("=== 5. :Names — defaults 키 + Of가 만든 키, 메소드는 안 셈, 팬텀 필드도 없음 ===") do local s = Store({ hp = Source(1), name = Source("n") }) local function set(list: { string }): { [string]: true } local t = {} for _, k in list do t[k] = true end return t end local names = set(s:Names()) assert(names.hp and names.name and not names.Of and not names.Names and not names.__reservedCheck, "declared keys only") assert(#s:Names() == 2, "count") s:Of("dyn") assert(set(s:Names()).dyn == true and #s:Names() == 3, "Of adds to Names") assert(rawget(s, "__reservedCheck") == nil, "phantom field has no runtime counterpart") print("PASS") end print() print("=== 6. 빈 Store (H-83/H-157) — 무인자도 유효 ===") do local e = Store({} :: {}) assert(#e:Names() == 0, "no names") local d: Source = e:Of("x") assert(Quad.isSource(d), "Of works on an empty store") local e2 = (Quad.Store :: any)() assert(Quad.isStore(e2) and #e2:Names() == 0, "no-arg form (or {} guard)") print("PASS") end print() print("=== 7. 인스턴스별 팩토리 (H-174) — Of가 자기 quad의 Source를 만든다 ===") do local other = Quad.New() local s = other.Store({} :: {}) local d = s:Of("x") assert(getmetatable(d :: any) == getmetatable(other.Source(0) :: any), "Of uses the module's own Source, read at call time") assert(getmetatable(d :: any) ~= getmetatable(Quad.Source(0) :: any), "not the top-level instance's") print("PASS") end print() print("=== ALL PASS ===")