체크포인트 재개 첫 항목. 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
95 lines
3.5 KiB
Text
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
|