quad/quad-base/src/Source.luau
qwreey-agent-selene dc81dd26db
feat: 단위 2 /code-review high 반영 — ① 여섯(H-199/H-201/H-202/H-204/H-206/H-207) + ② 넷(H-198/H-200/H-203/H-205) §4 합류
체크포인트 재개 첫 항목. 8각도 후보 22 → 검증 생존 10(정확성 8건 실측 재현).

- H-199: With/Compute nil dep 조용한 탈락 → collectDeps + "dep #N is nil"(level 3)
- H-201: store:Of 이름 문자열 검증(할당 전, level 2)
- H-202: Compute fn 함수 검증(형제 표면과 같은 급)
- H-204: Store defaults 평범한 테이블 검증(clone 전)
- H-206: implsOf 세 벌 → ImplRegistry.luau 신설(잎, 내부 전용)
- H-207: Source.Set이 Impl.Emit 직접 호출로 위임(꼬리 한 벌)
- ② 넷은 코드에 TODO 마커만, 문항은 round11.md §4(마커 10 = 문항 10, 1:1)
- H-198(🔴 닫힌 게이트 너머 fn 도중 Set → 영구 stale)은 state-epoch-plan §4
  확정 의사코드 자체의 구멍 — 그 절에 ⚠️ 결정 대기 배너, README 색인 갱신
- architecture.md: ImplRegistry 소스 트리 등재, error 계약 "도착지가 계약" 명료화
- spec.state 13절·spec.store 2·3절 신설/확장, 감사 2라운드(6건 → 니트 2) 반영

Co-authored-by: qwreey <me@qwreey.moe>
Claude-Session: https://claude.ai/code/session_01LF78pXeFGD1ZSVD3ifteYG
2026-08-31 11:39:42 +09:00

95 lines
3.5 KiB
Text

--[[
Source — the writable root value. Structurally satisfies `State` (all
derived methods come from `State.luau`'s impl through `__index`) and is
itself an `Epoch` (`Revision`), so it is registered in BOTH
`SourceBrand` and `EpochBrand` (multi-tagging, `brand-plan.md`) — never in
`StateBrand`: `isState = isSource or StateBrand` is composed in the
predicate, not by double registration.
Sources, as-is:
- `.claude/base/source-state-plan.md` "State는 쓰기 대상이 아님 —
확정, Source는 독립 공개 프리미티브로 격상" (`Source(default)`),
"`Source:Set(v)`는 동일값이어도 항상 갱신하고 emit한다" (`H-68`),
"Source 값을 직접 mutate한 뒤 전파 — `:Emit()`" (root only),
"따름정리 — `Store<T>`/`Source<T>`의 `T`는 Modifier가 될 수 없음".
- `.claude/base/modifier-plan.md` 7번: `isModifier` guard at the
constructor and at `:Set` (the constructor also covers `store:Of`).
- `.claude/base/state-epoch-plan.md` §2: `Revision` bump is
`bit32.bnot(-rev)`; `Set`/`Emit` push the Source itself as the emit
payload ("출처" only — no value, no revision).
Assembly (`H-174`): `InitSource(module)` — needs the per-instance State
impl for its `__index` chain, and pulls it in itself via
`module:RunInit(State.Init)` (idempotent), so `New()` order is irrelevant.
]]
local Brand = require("./Brand")
local State = require("./State")
local QuadTypes = require("../roblox_packages/quad_types")
export type Source<T> = QuadTypes.Source<T>
local SourceBrand = Brand.SourceBrand
local EpochBrand = Brand.EpochBrand
local isModifier = Brand.isModifier
local WEAK_KEY_MT = { __mode = "k" }
local function Init(module: any)
-- Pull our dependency ourselves, `require`-style (`module-lifecycle-plan.md`
-- "순서 의존성은 각 `InitXxx`를 `require`처럼 멱등하게 만들어서 해소한다") —
-- `RunInit` is idempotent, so `New()` need not order us after `State.Init`.
module:RunInit(State.Init)
local StateImpl = State.implFor(module)
local emitDown = StateImpl._emitDown
local Impl = setmetatable({}, { __index = StateImpl })
Impl.__index = Impl
function Impl.Get(self: any): any
return self._value
end
-- In-place mutation was done by the caller; only the signal is sent.
-- Root Sources only — derived States have no such concept ("하드 경계").
function Impl.Emit(self: any): any
self.Revision = bit32.bnot(-self.Revision)
emitDown(self, self) -- payload = the source epoch itself
return self
end
-- `H-68`: same value still bumps the revision and emits — dedup is the
-- downstream's job (EpochMap judgement, gates), and `==` would silently
-- drop in-place table mutations.
function Impl.Set(self: any, value: any): any
if isModifier(value) then
error("Source: cannot Set a Modifier as a Source value", 2)
end
self._value = value
-- `H-207`: one copy of the bump-and-emit tail. Direct call, not colon
-- delegation — same type, same impl table, no subtype override to hit.
return Impl.Emit(self)
end
function Impl._track(self: any, map: any)
map:Sync(self) -- a Source IS an Epoch: it goes into the map directly
end
local function Source(default: any): any
if isModifier(default) then
error("Source: cannot hold a Modifier as a Source value", 2)
end
local self = setmetatable({
_value = default,
Revision = 0,
_subs = setmetatable({}, WEAK_KEY_MT), -- weak-key subscribers (downstream)
}, Impl)
SourceBrand:register(self)
EpochBrand:register(self)
return self
end
module.Source = Source
end
return Init